interactionintermediate

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

Default
Hover
Active
Focus
Disabled
Loading

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.

Common mistakes

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).