Interaction states
Interaction states define how UI elements visually respond to user actions: hover, focus, active (pressed), and disabled. Consistent interaction states help users understand what's clickable, what's focused, and what's unavailable.
The 4 core states
| State | Trigger | Purpose |
|---|---|---|
| Hover | Mouse over element | Indicates interactivity |
| Focus | Keyboard navigation / click | Shows which element is active |
| Active | Mouse down / pressing | Confirms the click is registering |
| Disabled | Element unavailable | Prevents interaction |
State patterns by component
Buttons
| State | Visual change | Tailwind classes |
|---|---|---|
| Hover | Background darkens 10% | hover:bg-primary/90 |
| Focus | Ring appears (keyboard only) | focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 |
| Active | Scale down + darken | active:bg-primary/80 active:scale-[0.98] |
| Disabled | Opacity 50% + cursor | disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none |
Key rule: Use focus-visible instead of focus for buttons. This shows the focus ring only for keyboard users, not mouse clicks.
Inputs
| State | Visual change | Tailwind classes |
|---|---|---|
| Hover | Border darkens | hover:border-muted-foreground/50 |
| Focus | Ring appears | focus:ring-1 focus:ring-ring focus:outline-none |
| Disabled | Muted background + opacity | disabled:opacity-50 disabled:bg-muted disabled:cursor-not-allowed |
Key rule: Inputs use focus (not focus-visible) because users need to see which field is active at all times.
Interactive cards
| State | Visual change | Tailwind classes |
|---|---|---|
| Hover | Lift + shadow | hover:shadow-md hover:-translate-y-0.5 transition-all |
| Focus | Ring appears | focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 |
| Active | Depress | active:scale-[0.99] active:shadow-sm |
Key rule: Non-interactive cards should have NO hover effect. This avoids false affordance.
Links
| State | Visual change | Tailwind classes |
|---|---|---|
| Hover | Underline | hover:underline |
| Focus | Ring appears | focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 |
Global rules
1. Hover feedback should be immediate
Hover states need instant feedback (under 50ms). Use transition-colors with short duration.
// Good — fast transition
className="transition-colors duration-50 hover:bg-primary/90"
// Bad — too slow
className="transition-all duration-300 hover:bg-primary/90"
2. Use focus-visible for keyboard focus
For buttons, links, and cards, use focus-visible to show focus rings only for keyboard navigation.
// Good — keyboard users only see ring
className="focus-visible:ring-2 focus-visible:ring-ring"
// Bad — mouse users see ring on every click
className="focus:ring-2 focus:ring-ring"
3. Disabled states need three things
- Visual indicator:
disabled:opacity-50 - Cursor feedback:
disabled:cursor-not-allowed - Prevent interaction:
disabled:pointer-events-none
// Complete disabled state
className="disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none"
// Incomplete — hover effects still work
className="disabled:opacity-50 disabled:cursor-not-allowed"
4. Keep hover effects subtle
Hover changes should be 5-10% opacity/lightness shifts, not dramatic transforms.
// Good — subtle
className="hover:bg-primary/90" // 10% darker
// Bad — too dramatic
className="hover:scale-110 hover:rotate-2"
MCP tool usage
Getting interaction states
// Get all states for a button
get_interaction_states({ component: 'button' })
// Get specific state
get_interaction_states({
component: 'button',
variant: 'primary',
state: 'hover'
})
// Include rules for learning
get_interaction_states({
component: 'input',
includeRules: true
})
Validating implementation
// Check if a component has all required states
validate_interaction_states({
code: '',
component: 'button'
})
// Returns missing states, violations, and recommendations
Common mistakes
Using focus instead of focus-visible
// Problem: Ring shows on mouse click
// Solution: Ring only on keyboard focus
Incomplete disabled states
// Problem: Hover still works when disabled
// Solution: Disable pointer events
Static cards with hover effects
// Problem: Card looks clickable but isn't
Content
// Solution: No hover on non-interactive elements
Content
FAQ
When should I use focus vs focus-visible?
- focus-visible: buttons, links, cards, tabs (mouse users don't need focus indication)
- focus: inputs, textareas, selects (users need to see which field is active)
Should all interactive elements have all four states?
Not necessarily:
- Buttons: hover, focus-visible, active, disabled
- Inputs: hover, focus, disabled (no active needed)
- Links: hover, focus-visible (active optional)
- Static cards: none (they're not interactive)
How dark should hover be?
10% is the sweet spot. Use /90 opacity in Tailwind:
hover:bg-primary/90(10% darker)hover:text-primary/90(10% darker text)
Going darker (e.g., /70) is too dramatic for most cases.