Skip to content
Development
Skill

/button-states

Rest, hover, active, focus, disabled and loading states, derived from the base colour. Use when designing buttons, links, inputs or any clickable component.

From plugin
dembrandt
18045 skills1 MCP

Claude Code

$ npx -y skills add dembrandt/dembrandt-skills --skill button-states --agent claude-code

SKILL.md

button-states.SKILL.md
name: button-states
description: Rest, hover, active, focus, disabled and loading states, derived from the base colour. Use when designing buttons, links, inputs or any clickable component.
metadata:
  priority: 8
  pathPatterns:
    - "components/**"
    - "src/components/**"
    - "**/*.css"
    - "**/*.scss"
    - "**/tokens/**"
    - "**/*.tsx"
    - "**/*.jsx"
    - "design-system/**"
  promptSignals:
    phrases:
      - "hover state"
      - "active state"
      - "focus state"
      - "disabled"
      - "button states"
      - "interactive states"
      - "loading state"
      - "pressed"
retrieval:
  aliases:
    - button states
    - hover state
    - interactive states
    - focus ring
    - disabled state
    - loading button
  intents:
    - design button states
    - add hover and active states
    - style focus ring
    - handle disabled state
    - add loading state to button
  examples:
    - what colour should the hover state be
    - design all states for this button
    - how do I show a loading state on a button
    - make the focus ring visible

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.

base:     hsl(H, S%, L%)
hover:    hsl(H, S%, L% - 8%)    ← darken 8%
active:   hsl(H, S%, L% - 14%)   ← darken 14%

Example: primary button `#635BFF` (hsl 243, 100%, 68%)

.btn-primary {
  background: hsl(243, 100%, 68%);       /* rest    #635BFF */
}
.btn-primary:hover {
  background: hsl(243, 100%, 60%);       /* hover   #4A40FF */
}
.btn-primary:active {
  background: hsl(243, 100%, 54%);       /* active  #3429FF */
}

For light buttons on dark backgrounds, invert the logic — lighten on hover instead of darkening.

Secondary / outlined buttons

.btn-secondary {
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text);
}
.btn-secondary:hover {
  background: var(--color-grey-100);     /* subtle fill */
  border-color: var(--color-grey-300);
}
.btn-secondary:active {
  background: var(--color-grey-200);
}

The Primary Fill Is Chosen per Background

A brand's main action is often not one colour. The bright brand colour carries it on white, a dark variant of the same hue on a light brand tint, black or white on photos and dark sections. Define the primary button as one role with a fill per surface it sits on, each with its own derived states and a contrast check, rather than forcing one fill onto every background. A near-black or desaturated colour of the brand hue is a variant in the brand family, not neutral ink.

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.

.btn:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 3px;
}
  • 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

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}
  • 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.

.btn--loading {
  pointer-events: none;
  opacity: 0.7;
}

**The cursor does not change while loading.** It signals affordance, not progress: the arrow for ordinary content, `pointer` for something you can act on. Progress belongs to the element and the loading region, which the user is already looking at, rather than to wherever the mouse happens to rest. `cursor: wait` is also inert next to `pointer-events: none`, so the rule reads as deliberate while doing nothing.

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

.btn:active {
  transform: scale(0.97);
  transition: transform 80ms ease-out;
}

Keep the scale value between `0.95–0.98`. Below `0.95` feels like the button is breaking.

Complete Button CSS Reference

.btn {
  cursor: pointer;
  background: var(--color-primary);
  color: white;
  border-radius: var(--radius-button);
  padding: var(--component-padding-y-md) var(--component-padding-x-md);
  height: var(--component-height-md);
  border: none;
  transition: background 120ms ease-out, transform 80ms ease-out;
}

.btn:hover           { background:
Read more
@dembrandt
Ships withdembrandt

Enterprise UX secrets, now built into your AI agent. Your agent writes the component in seconds. It does not know the modal should be a drawer, or that the grey it picked fails contrast. These skills carry that judgment: what to do, and what breaks otherwise.

18045 skills1 MCP
Get the whole plugin

Stars on GitHub

Momentum

stars in total
180
Star history of dembrandt/dembrandt-skills on GitHub, a chart by star-history.com

Other skills on dembrandt.

app-shell

app-shell

Top bar, sidebar rail, app launcher, tenant and environment cue, status bar. Use when designing the persistent shell, a…

@dembrandt@dembrandtInstall Skill
brand-visual-language

brand-visual-language

Shape language across type, radius and icons carries brand personality. Use when starting a design system, picking icons or…

@dembrandt@dembrandtInstall Skill
clone-website

clone-website

Rebuild a live web page 1:1 from measurement into Figma, Penpot or code, then verify it. Use when reproducing a page or checking…

@dembrandt@dembrandtInstall Skill
color-mode-and-theme

color-mode-and-theme

Light, dark or both, and when a theme selector is worth it. Use when defining a base palette or deciding on dark mode support.

@dembrandt@dembrandtInstall Skill

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills11d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc13d293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react2d252k
@facebook@facebookInstall Skill

Questions about button-states.

Which coding agents can use button-states?
button-states installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.