button-states
Every interactive element needs a complete set of visual states — rest, hover, active/pressed, focus, disabled, and loading. States should be derived algorithmically from the base colour, not chosen arbitrarily. Use when designing buttons, links, inputs, or any clickable component.
By dembrandt · 643 installs
npx skills add dembrandt/dembrandt-skills --skill button-states
Source repository · Upstream listing
Button and Interactive Element States
Every interactive component must have a complete, visually distinct state for each interaction mode. Missing or ambiguous states make the UI feel unfinished and reduce user confidence.
The Six States
State Trigger Visual signal
Rest Default Base colour, cursor: pointer
Hover Mouse over Slightly darker, subtle background shift
Active / Pressed Mouse down / tap Noticeably darker, slight scale down
Focus Keyboard navigation Visible focus ring, no change to fill
Disabled Not available Low contrast, cursor: not allowed, no interaction
Loading Async action in progress Spinner or pulse, non interactive
Deriving State Colours Algorithmically
State colours are not chosen independently — they are derived from the base colour by adjusting lightness in HSL. This guarantees coherence across the entire palette.
Example: primary button 635BFF (hsl 243, 100%, 68%)
For light buttons on dark backgrounds, invert the logic — lighten on hover instead of darkening.
Secondary / outlined buttons
Focus State
Focus is a keyboard navigation requirement (WCAG 2.2). It must be visible and must not rely on the hover style alone — keyboard users do not trigger hover.
Use outline , not box shadow , for focus rings — outline respects border radius in modern browsers and does not affect layout
outline offset: 2–4px gives the ring breathing room from the component edge
Never use outline: none without a replacement focus style
Disabled State
Disabled elements are exempt from WCAG contrast requirements — low opacity is correct and intentional
Use pointer events: none to prevent click events even if JS is bypassed
Do not change the shape or size of a disabled button — only colour and cursor change
Loading State
When a button triggers an async action, replace the label with a spinner and prevent re submission.
Keep the button width stable during loading — avoid layout shift when label is replaced by spinner
Return to rest state on completion (success or error)
For long running operations, pair with a status message — a spinner alone does not tell the user what is happening
Scale on Active (Optional)
A subtle scale down on press adds physical feedback — borrowed from Disney's squash principle.
Keep the scale value between 0.95–0.98 . Below 0.95 feels like the button is breaking.
Complete Button CSS Reference
Review Checklist
[ ] Does every interactive element have all six states defined?
[ ] Are hover and active colours derived from the base by lightness adjustment (not chosen arbitrarily)?
[ ] Is focus state visible and using outline (not removed)?
[ ] Is disabled state low opacity with cursor: not allowed ?
[ ] Does loading state prevent re submission?
[ ] Are transition durations 80–150ms — not instant, not slow?
[ ] Does cursor: pointer appear on all interactive elements at rest?