Button States
The distinct visual appearances a button takes across its interactive lifecycle: default, hover, active, focus, disabled, and loading.
Plain English
Every button has a lifecycle. At rest it invites action. On hover it confirms "yes, I am clickable." On press (active) it gives physical feedback. With keyboard focus it shows a ring so non-mouse users know where they are. Disabled it communicates "not available right now." Loading it shows progress. Designing all six states is not over-engineering — missing any one of them creates confusing moments that erode trust in the product.
Technical
Button states map to CSS pseudo-classes: :hover, :active, :focus-visible, and :disabled. Loading is a custom state requiring a class toggle (e.g. is-loading) combined with aria-disabled="true" and a spinner. :focus-visible (not :focus) is preferred for keyboard-only focus rings, as :focus triggers on mouse clicks too — creating a jarring outline that designers then remove entirely, which breaks keyboard navigation.
Live Demo
All 6 Button States
Every interactive element needs all 6 states defined before shipping.
Usage
✓ Good usage
Implement all six states on every interactive button and test each with keyboard navigation — a user tabbing through a form should always see where focus is.
✗ Bad usage
Removing the focus ring entirely (:focus { outline: none } with no replacement) makes keyboard navigation invisible — a critical accessibility failure.
Recommended values
- Default: brand fill, white text, standard padding and radius
- Hover: 10–15% darker fill, subtle scale transform (1.01) or shadow increase
- Active: 15–20% darker fill, scale-down (0.98) for press sensation
- Focus-visible: 2px solid ring offset by 2px, matching brand accent
- Disabled: 40–50% opacity, cursor: not-allowed, no pointer events
- Loading: spinner replaces or precedes label, width locked to prevent layout shift
Common mistakes
- Using :focus instead of :focus-visible — :focus shows the ring on mouse clicks, designers remove it, keyboard users lose all focus indication.
- Skipping the active/pressed state — without it, the button feels unresponsive on click.
- Not locking button width during loading — the text shrinks when a spinner appears, causing layout shift.
AI Prompt Example
Copy this into Claude, Cursor, Bolt, or v0.
Design a primary button with 6 states: default (blue fill), hover (darker, slight scale up), active (darker, scale down), focus-visible (2px ring), disabled (50% opacity), and loading (spinner + locked width).