MetaMask / MetaMask/metamask-design-system
Standardize string value conventions for prop types across design system
- Dominant language
- TypeScript
- Stars
- 37
- Forks
- 14
- Avg merge
- 1d 9h
- Merged PRs (30d)
- 60
Description
### **Description**
Following the enum-to-string-union migration in #887, we've identified inconsistencies in how string values are formatted across the design system. Some use lowercase (`'primary'`), some use PascalCase (`'Account'`), and some use Capitalized (`'Primary'`).
This issue proposes standardizing all string prop values to follow a **single consistent convention** across the entire component API.
**Why this matters:**
- **Consistency**: Single rule for all prop values - no mental overhead
- **Developer Experience**: Predictable, uniform API
- **Maintainability**: One convention to document and enforce
- **Type Safety**: Clear patterns for new components
### **Current State**
**Inconsistencies Found:**
**React Package (`design-system-react`):**
- ✅ Button variants: `'primary'`, `'secondary'`, `'tertiary'` (lowercase - correct)
- ❌ AvatarGroup variants: `'Account'`, `'Favicon'`, `'Network'`, `'Token'` (PascalCase)
- ❌ BadgeWrapper positions: `'TopRight'`, `'BottomRight'`, `'BottomLeft'`, `'TopLeft'` (PascalCase)
- ❌ BadgeWrapper shapes: `'Rectangular'`, `'Circular'` (PascalCase)
- ✅ Sizes: `'xs'`, `'sm'`, `'md'`, `'lg'`, `'xl'` (lowercase - correct)
- ✅ Tailwind classes: `'bg-primary-default'`, `'text-icon-default'` (kebab-case - correct)
**React Native Package (`design-system-react-native`):**
- ❌ Button variants: `'Primary'`, `'Secondary'`, `'Tertiary'` (Capitalized)
- ❌ AvatarGroup variants: `'Account'`, `'Favicon'`, `'Network'`, `'Token'` (PascalCase)
- ❌ Badge sizes: `'Md'`, `'Lg'` (Capitalized)
- ❌ BadgeWrapper positions: `'TopRight'`, etc. (PascalCase)
- ❌ BadgeWrapper shapes: `'Rectangular'`, `'Circular'` (PascalCase)
- ✅ Avatar sizes: `'16'`, `'24'`, `'32'`, `'40'`, `'48'` (numeric strings - acceptable)
- ✅ Tailwind classes: kebab-case (correct)
### **Technical Details**
**Key Considerations:**
**1. Tailwind CSS Integration (Critical)**
Some prop values are actual Tailwind CSS class names that get passed to `twMerge`:
- `BoxBackgroundColor`: `'bg-primary-default'`, `'bg-error-muted'`
- `BoxBorderColor`: `'border-default'`, `'border-primary-inverse'`
- `TextColor`: `'text-default'`, `'text-primary-default'`
- `IconColor`: `'text-icon-default'`, `'text-primary-inverse'`
- `BoxFlexDirection`: `'flex-row'`, `'flex-col'`
**These Tailwind props already establish kebab-case as our convention.**
**2. Industry Standards Research**
Comprehensive analysis of major component libraries shows **100% use lowercase**:
| Library | Convention | Example |
|---------|-----------|---------|
| **Radix UI** | lowercase | `'solid'`, `'outline'`, `'ghost'` |
| **Radix Icons** | kebab-case | `'arrow-down'`, `'check-circle'` (15x15 SVGs, no props) |
| **Chakra UI** | lowercase + camelCase | `'solid'`, `'whiteAlpha'` |
| **shadcn/ui** | lowercase + kebab-case | `'default'`, `'icon-sm'` |
| **Material-UI** | lowercase | `'text'`, `'outlined'`, `'small'` |
| **Ant Design** | lowercase | `'primary'`, `'default'` |
| **Mantine** | lowercase + kebab-case | `'filled'`, `'compact-xs'` |
| **Blueprint.js** | lowercase | `'primary'`, `'success'` |
**Note:** While most use lowercase/camelCase for variants, Radix Icons (the actual icon names) and shadcn use kebab-case for multi-word values.
**3. The Consistency Argument**
**Most Important Consideration:** Having a **single convention across the entire API** is more valuable than micro-optimizations for individual cases.
**Keys vs Values:**
```typescript
export const IconName = {
ArrowDown: 'arrow-down', // KEY (code) vs VALUE (runtime)
// ↑ PascalCase ↑ kebab-case
// Used in code Passed at runtime
};
// Developer experience comes from the KEY:
// ✅ Clean dot notation
// ✅ String literal also works
// Destructuring uses KEYS:
const { ArrowDown, CheckCircle } = IconName; // ✅ Perfect!
```
### **Proposed Convention**
**Complete Kebab-Case Strategy for Total Consistency:**
1. **ALL prop string values**: kebab-case
- Single-word: `'primary'`, `'secondary'`, `'account'`, `'favicon'`
- Multi-word: `'arrow-down'`, `'check-circle'`, `'top-right'`, `'bottom-left'`
- Tailwind: `'bg-primary-default'`, `'text-icon-default'` (already kebab-case!)
2. **Const object keys**: PascalCase (for code DX)
```typescript
export const ButtonVariant = {
Primary: 'primary', // PascalCase key, kebab value
Secondary: 'secondary',
} as const;
export const IconName = {
ArrowDown: 'arrow-down',
CheckCircle: 'check-circle',
} as const;
export const BadgePosition = {
TopRight: 'top-right',
BottomLeft: 'bottom-left',
} as const;
```
**Rationale:**
- ✅ **100% consistent** - Single rule: "all prop values are kebab-case"
- ✅ **Tailwind alignment** - Matches existing `BoxBackgroundColor`, `TextColor`, etc.
- ✅ **HTML/CSS standards** - Aligns with `data-*`, `aria-*`, CSS class conventions
- ✅ **Serialization-friendly** - JSON, URLs, databases prefer kebab-case
- ✅ **Keys provide DX** - PascalCase keys give clean `IconName.ArrowDown` syntax
- ✅ **Values provide consistency** - All runtime values follow one pattern
**Why kebab-case over camelCase?**
- Our Tailwind classes (`'bg-primary-default'`) have already set the precedent
- Web platform conventions (HTML attributes, CSS classes) use kebab-case
- More universal for external systems (APIs, databases, config files)
### **Tradeoffs**
**Potential Awkwardness: Bracket Notation**
If you need to access const object values using the runtime string, you must use bracket notation:
```typescript
// ❌ Potentially awkward (but when would we do this?)
const iconValue = 'arrow-down'; // From API/database
const icon = IconName['arrow-down']; // Must use brackets
// ✅ In practice, you'd map by searching:
const iconKey = Object.entries(IconName).find(
([key, value]) => value === iconValue
)?.[0];
const icon = IconName[iconKey]; // 'ArrowDown'
// ✅ Or more commonly, just use the value directly:
// 'arrow-down' works!
```
**When would you actually need `IconName['arrow-down']`?**
Likely never! The typical patterns are:
1. **Code reference**: `IconName.ArrowDown` → `'arrow-down'` (uses KEY)
2. **String literal**: `` (direct value)
3. **Dynamic from API**: `` (pass through)
You rarely need to go backwards from value to const object entry.
**If this DOES become a real use case**, we could provide a helper:
```typescript
// Helper for reverse lookup (only if needed)
export const getIconNameKey = (value: string) => {
return Object.entries(IconName).find(
([, val]) => val === value
)?.[0];
};
```
### **Values to Change**
**React Package:**
```typescript
// Button variants (already correct!)
✅ Keep: 'primary', 'secondary', 'tertiary'
// AvatarGroup variants
- 'Account' → 'account'
- 'Favicon' → 'favicon'
- 'Network' → 'network'
- 'Token' → 'token'
// BadgeWrapper positions
- 'TopRight' → 'top-right'
- 'BottomRight' → 'bottom-right'
- 'BottomLeft' → 'bottom-left'
- 'TopLeft' → 'top-left'
// BadgeWrapper shapes
- 'Rectangular' → 'rectangular'
- 'Circular' → 'circular'
// Icon names (279 icons!)
- 'Accessibility' → 'accessibility'
- 'ArrowDown' → 'arrow-down'
- 'CheckCircle' → 'check-circle'
- ... (all 279)
```
**React Native Package:**
```typescript
// Button variants
- 'Primary' → 'primary'
- 'Secondary' → 'secondary'
- 'Tertiary' → 'tertiary'
// Badge sizes
- 'Md' → 'md'
- 'Lg' → 'lg'
// AvatarGroup + BadgeWrapper + Icons (same as React above)
```
**Files to Update:**
- `packages/design-system-react/src/types/index.ts`
- `packages/design-system-react-native/src/types/index.ts`
- `packages/design-system-react/src/components/AvatarGroup/AvatarGroup.types.ts`
- `packages/design-system-react-native/src/components/AvatarGroup/AvatarGroup.types.ts`
- Icon generation scripts (to output kebab-case)
- All component files using these values
- Storybook stories
- Tests
- Documentation
### **Acceptance Criteria**
**Type Definitions:**
- [ ] All prop string values use kebab-case consistently:
- Single-word: `'primary'`, `'account'`, `'solid'`
- Multi-word: `'arrow-down'`, `'top-right'`, `'check-circle'`
- [ ] Const object keys use consistent PascalCase
- [ ] Icon generation script updated to output kebab-case values
**Code Updates:**
- [ ] Update all type definitions in both packages
- [ ] Update discriminated union types in component `.types.ts` files
- [ ] Update all component implementations
- [ ] Update all Storybook stories
- [ ] Update all tests
**Documentation:**
- [ ] Update `ENUM_MIGRATION_EXAMPLES.md` with kebab-case convention
- [ ] Create `STRING_VALUE_CONVENTIONS.md` documenting the standard
- [ ] Document the "keys for code, values for consistency" pattern
- [ ] Add migration guide for consumers in CHANGELOG
**Verification:**
- [ ] All TypeScript builds pass (`yarn tsc`)
- [ ] All tests pass (`yarn test`)
- [ ] All linting passes (`yarn lint`)
- [ ] Tailwind integration still works
- [ ] Storybook builds and displays correctly
**Migration Support:**
- [ ] Document breaking changes clearly
- [ ] Provide before/after examples
- [ ] Consider creating a codemod script for consumers
- [ ] Bump to next major version
### **References**
- PR #887: Enum to string union migration (where inconsistencies were introduced)
- Issue #883: Original enum migration issue
- [Radix UI Documentation](https://www.radix-ui.com/)
- [Radix Icons](https://www.radix-ui.com/icons) - Uses kebab-case for icon names
- [Chakra UI Documentation](https://chakra-ui.com/)
- [shadcn/ui Documentation](https://ui.shadcn.com/) - Uses kebab-case for modifiers
- [Material-UI Documentation](https://mui.com/)
**Industry Research Summary:**
All major React component libraries use lowercase for prop values. Our Tailwind integration (kebab-case) has already set the precedent. Full kebab-case provides **complete consistency** across our entire API.
**Key Insight:**
The "awkward bracket notation" concern (`IconName['arrow-down']`) is theoretical - in practice, you access via keys (`IconName.ArrowDown`) or use string literals directly (`"arrow-down"`). The consistency gain outweighs this non-issue.
---
**Breaking Change Note:** This will be a breaking change requiring a major version bump. However, it provides **total consistency** across the design system and aligns with web platform conventions.
Contributor guide
Research direction
Start by reviewing the type files in both design-system packages and the icon generation scripts, then trace the listed values through component files, Storybook stories, tests, and documentation. The work is done when the prop values and generated icons follow the documented convention, consumer migration guidance is present, and the TypeScript build, tests, lint, and Storybook verification pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- storybook, tailwindcss, typescript
- Domain
- design, documentation, frontend
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 30/100