States & Variants Standards
Components exist in multiple states and variants. States represent interactive conditions (hover, focus, disabled), while variants represent visual style options (primary, secondary, ghost). Clearly defining and documenting these ensures consistent implementation and predictable user experiences.
Why States & Variants Matter
Well-defined states and variants:
- Consistency: Same states work the same way across components
- Predictability: Users know what to expect
- Accessibility: States communicate meaning to all users
- Maintainability: Clear definitions prevent drift
Component States
Default State
The base, unmodified state of a component:
// Default button state
<Button>Click me</Button>
// Default state styling
.button {
background: var(--semantic-color-background-primary);
color: var(--semantic-color-foreground-primary);
}Interactive States
States triggered by user interaction:
- Hover: Mouse pointer over element
- Focus: Element has keyboard focus
- Active: Element is being activated (click/press)
- Pressed: Element is in pressed state (toggle buttons)
// Interactive states
.button {
&:hover {
background: var(--semantic-color-background-primary-hover);
}
&:focus-visible {
outline: 2px solid var(--semantic-color-border-focus);
outline-offset: 2px;
}
&:active {
background: var(--semantic-color-background-primary-active);
}
&[aria-pressed="true"] {
background: var(--semantic-color-background-primary-pressed);
}
}Functional States
States related to component functionality:
- Disabled: Component is not interactive
- Loading: Component is processing an action
- Error: Component has an error state
- Success: Component indicates successful action
// Functional states
<Button disabled>Disabled</Button>
<Button loading>Loading...</Button>
<Input error="Invalid email" />
<Input success />
// State styling
.button {
&:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
&[aria-busy="true"] {
opacity: 0.7;
cursor: wait;
}
}State Combinations
Components can have multiple states simultaneously:
// Multiple states
<Button disabled loading>Processing...</Button>
// Handle state combinations
.button {
&:disabled {
// Disabled takes precedence
pointer-events: none;
&:hover {
// Disabled buttons don't hover
background: inherit;
}
}
}Component Variants
Visual Variants
Variants that change visual appearance:
// Button variants
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Danger</Button>
// Variant styling
.button {
&--primary {
background: var(--semantic-color-background-primary);
color: var(--semantic-color-foreground-on-primary);
}
&--secondary {
background: var(--semantic-color-background-secondary);
color: var(--semantic-color-foreground-on-secondary);
}
&--ghost {
background: transparent;
border: 1px solid var(--semantic-color-border-default);
}
&--danger {
background: var(--semantic-color-background-error);
color: var(--semantic-color-foreground-on-error);
}
}Size Variants
Variants that change component size:
// Size variants
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
// Size styling
.button {
&--sm {
padding: var(--semantic-spacing-padding-button-sm);
font-size: var(--semantic-typography-size-button-sm);
}
&--md {
padding: var(--semantic-spacing-padding-button-md);
font-size: var(--semantic-typography-size-button-md);
}
&--lg {
padding: var(--semantic-spacing-padding-button-lg);
font-size: var(--semantic-typography-size-button-lg);
}
}State Documentation
Required Documentation
Every component should document:
- All States: Default, interactive, functional
- Visual Examples: Show each state visually
- Trigger Conditions: What causes each state
- State Combinations: How states interact
- Accessibility: How states are communicated
State Matrix
Document all state combinations:
/**
* Button State Matrix
*
* States: default | hover | focus | active | disabled | loading
* Variants: primary | secondary | ghost | danger
*
* Valid combinations:
* - primary + default
* - primary + hover
* - primary + focus
* - primary + active
* - primary + disabled
* - primary + loading
* - secondary + default
* - ... (all combinations)
*
* Invalid combinations:
* - disabled + hover (disabled prevents hover)
* - disabled + active (disabled prevents active)
*/Variant Patterns
Consistent Variant Names
Use consistent variant names across components:
// ✅ Good: Consistent naming
<Button variant="primary" />
<Input variant="primary" />
<Card variant="primary" />
// ❌ Bad: Inconsistent naming
<Button variant="primary" />
<Input appearance="primary" />
<Card style="primary" />Semantic Variants
Variants should describe purpose, not appearance:
// ✅ Good: Semantic variants
<Button variant="primary" /> // Main action
<Button variant="secondary" /> // Secondary action
<Button variant="danger" /> // Destructive action
// ❌ Bad: Appearance-based variants
<Button variant="blue" />
<Button variant="outlined" />
<Button variant="solid" />State Management
Controlled vs Uncontrolled
Document whether component manages its own state:
// Uncontrolled: Component manages state
<Button>Click me</Button>
// Controlled: Parent manages state
<Button
disabled={isDisabled}
loading={isLoading}
onClick={handleClick}
>
Click me
</Button>State Props
Use boolean props for binary states:
interface ButtonProps {
disabled?: boolean;
loading?: boolean;
active?: boolean;
}
// Clear, predictable state management
<Button disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save'}
</Button>Visual Documentation
State Examples
Visual examples help teams understand states:
- Interactive Showcase: Live examples users can interact with
- State Grid: Grid showing all state combinations
- Before/After: Comparison of state transitions
Common Pitfalls
1. Missing States
// ❌ Bad: Missing focus state
.button {
&:hover {
background: blue;
}
// No focus state!
}
// ✅ Good: All states defined
.button {
&:hover {
background: blue;
}
&:focus-visible {
outline: 2px solid blue;
}
}2. Inconsistent State Behavior
// ❌ Bad: Different disabled behavior
.button.disabled { opacity: 0.5; }
.input.disabled { opacity: 0.3; } // Inconsistent!
// ✅ Good: Consistent disabled behavior
.button:disabled,
.input:disabled {
opacity: 0.5;
cursor: not-allowed;
}3. State Conflicts
// ❌ Bad: States conflict
.button {
&:disabled {
opacity: 0.5;
}
&:hover {
background: blue; // Hover still works when disabled!
}
}
// ✅ Good: Disabled prevents hover
.button {
&:disabled {
opacity: 0.5;
pointer-events: none;
&:hover {
background: inherit; // No hover when disabled
}
}
}Related Resources
- Props & API Standards — How props control states and variants
- Accessibility Standards — How states support accessibility
- Design Tokens — How tokens define state styling