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