States and Variants
Component state definitions and variant patterns.
Interactive States
State Definitions
| State |
Trigger |
Visual Change |
| default |
None |
Base appearance |
| hover |
Mouse over |
Slight color shift |
| focus |
Tab/click |
Focus ring |
| active |
Mouse down |
Darkest color |
| disabled |
disabled attr |
Reduced opacity |
| loading |
Async action |
Spinner + opacity |
State Priority
When multiple states apply, priority (highest to lowest):
- disabled
- loading
- active
- focus
- hover
- default
State Transitions
/* Standard transition for interactive elements */
.interactive {
transition-property: color, background-color, border-color, box-shadow;
transition-duration: var(--duration-fast);
transition-timing-function: ease-in-out;
}
| Transition |
Duration |
Easing |
| Color changes |
150ms |
ease-in-out |
| Background |
150ms |
ease-in-out |
| Transform |
200ms |
ease-out |
| Opacity |
150ms |
ease |
| Shadow |
200ms |
ease-out |
Focus States
Focus Ring Spec
/* Standard focus ring */
.focusable:focus-visible {
outline: none;
box-shadow: 0 0 0 var(--ring-offset) var(--color-background),
0 0 0 calc(var(--ring-offset) + var(--ring-width)) var(--ring-color);
}
| Property |
Value |
| Ring width |
2px |
| Ring offset |
2px |
| Ring color |
primary (blue-500) |
| Offset color |
background |
Focus Within
/* Container focus when child is focused */
.container:focus-within {
border-color: var(--color-ring);
}
Disabled States
Visual Treatment
.disabled {
opacity: var(--opacity-disabled); /* 0.5 */
pointer-events: none;
cursor: not-allowed;
}
| Property |
Disabled Value |
| Opacity |
50% |
| Pointer events |
none |
| Cursor |
not-allowed |
| Background |
muted |
| Color |
muted-foreground |
Accessibility
- Use
aria-disabled="true" for semantic disabled
- Use
disabled attribute for form elements
- Maintain sufficient contrast (3:1 minimum)
Loading States
Spinner Placement
| Component |
Spinner Position |
| Button |
Replace icon or center |
| Input |
Trailing position |
| Card |
Center overlay |
| Page |
Center of viewport |
Loading Treatment
.loading {
position: relative;
pointer-events: none;
}
.loading::after {
content: '';
/* spinner styles */
}
.loading > * {
opacity: 0.7;
}
Error States
Visual Indicators
.error {
border-color: var(--color-error);
color: var(--color-error);
}
.error:focus-visible {
box-shadow: 0 0 0 2px var(--color-background),
0 0 0 4px var(--color-error);
}
| Element |
Error Treatment |
| Input border |
red-500 |
| Input focus ring |
red/20% |
| Helper text |
red-600 |
| Icon |
red-500 |
Error Messages
- Position below input
- Use error color
- Include icon for accessibility
- Clear on valid input
Variant Patterns
Color Variants
/* Pattern for color variants */
.component {
--component-bg: var(--color-primary);
--component-fg: var(--color-primary-foreground);
background: var(--component-bg);
color: var(--component-fg);
}
.component.secondary {
--component-bg: var(--color-secondary);
--component-fg: var(--color-secondary-foreground);
}
.component.destructive {
--component-bg: var(--color-destructive);
--component-fg: var(--color-destructive-foreground);
}
Size Variants
/* Pattern for size variants */
.component {
--component-height: 40px;
--component-padding: var(--space-4);
--component-font: var(--font-size-sm);
}
.component.sm {
--component-height: 32px;
--component-padding: var(--space-3);
--component-font: var(--font-size-xs);
}
.component.lg {
--component-height: 48px;
--component-padding: var(--space-6);
--component-font: var(--font-size-base);
}
Accessibility Requirements
Color Contrast
| Element |
Minimum Ratio |
| Normal text |
4.5:1 |
| Large text (18px+) |
3:1 |
| UI components |
3:1 |
| Focus indicator |
3:1 |
State Indicators
- Never rely on color alone
- Use icons, text, or patterns
- Ensure focus is visible
- Provide loading announcements
ARIA States
<!-- Disabled -->
<button disabled aria-disabled="true">Submit</button>
<!-- Loading -->
<button aria-busy="true" aria-describedby="loading-text">
<span id="loading-text" class="sr-only">Loading...</span>
</button>
<!-- Error -->
<input aria-invalid="true" aria-describedby="error-msg">
<span id="error-msg" role="alert">Error message</span>