Skip to main content
skill CX Codex

button-states — Claude Code skill for button states

A skill that defines six states for interactive elements and derives hover and active colours from the base colour in HSL. Useful when buttons in a project look unfinished.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add dembrandt/dembrandt-skills --skill button-states -a claude-code
Open on GitHub

What button-states does

The skill covers six states: rest, hover, active (pressed), focus, disabled and loading. Each one gets a trigger and a visual signal, from the pointer cursor at rest to a spinner during an async action. Hover and active colours are not picked by eye — they come from the base colour by shifting lightness in HSL, hover 8% darker, active 14% darker. Light buttons on dark backgrounds invert that logic and lighten on hover.

Inside are ready CSS blocks: primary and secondary buttons, focus via outline with a 2–4px offset, disabled at opacity: 0.4 with pointer-events: none, and loading that keeps the button width stable so the layout does not jump. The press scale gets its own section: keep it between 0.95 and 0.98, anything lower reads as a broken button. The skill activates on paths like components/**, **/*.css and **/*.tsx, and on phrases such as "hover state" or "loading state".

A visible focus ring is a keyboard requirement, and hover styling cannot stand in for it because keyboard users never trigger hover. So outline: none without a replacement is treated as a bug. The cursor stays unchanged while an action runs: progress belongs to the element itself, not to wherever the mouse rests. An eight-item review checklist closes the skill.

What it helps with

  • derive hover and active colours from a base colour
  • add a visible focus ring for keyboard navigation
  • show a loading state and block double submission
  • make disabled buttons consistent across components
  • review interactive states before shipping a component

Example prompts for the agent

  • what colour should the hover state be
  • design all states for this button
  • how do I show a loading state on a button

How to install button-states

1Skills CLI (Vercel)

npx skills add dembrandt/dembrandt-skills --skill button-states -a claude-code

2Manually: put the files in place

~/.claude/skills/button-states/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/button-states/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about button-states

How do I install button-states in Claude?

Run npx skills add dembrandt/dembrandt-skills --skill button-states -a claude-code. The skill lands at ~/.claude/skills/button-states/SKILL.md.

What does button-states do?

It defines six states for buttons and links and shows how to derive hover and active colours from the base colour in HSL. It also ships CSS for focus, disabled and loading.

Where does button-states break?

On outline: none with no replacement — the focus ring disappears and keyboard navigation becomes invisible. It also advises against changing the cursor during loading, since the element itself shows progress.

Source text

SKILL.md · MIT
---
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%)

```css
.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

```css
.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);
}
```

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

```css
.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

```css
.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.

```css
.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.

```css
.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

```css
.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: var(--color-primary-hover); }
.btn:active          { background: var(--color-primary-active); transform: scale(0.97); }
.btn:focus-visible   { outline: 2px solid var(--color-primary); outline-offset: 3px; }
.btn:disabled        { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
.btn.btn--loading    { opacity: 0.7; pointer-events: none; }
```

## 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?
- [ ] Does the cursor stay unchanged while an action runs, with progress shown by the element itself?

© dembrandt · MIT · dembrandt/dembrandt-skills

Similar skills

Everything for Frontend and UI →

From the same set38

user-flows-and-guided-paths Guides the creation of multi-step wizards, checkout sequences, and onboarding journeys. Helps maintain clear step progression without trapping users. CX Codex +1 879installs responsive-paradigms Sets responsive design guidelines for AI coding agents, treating mobile, tablet, and desktop as distinct interaction paradigms rather than a single scaled layout. CX Codex +1 875installs gestalt-ui-organisation A skill that lays out controls by Gestalt principles so users see what belongs together without reading labels. Useful for toolbars, forms, navigation and dashboards. CX Codex +1 879installs visual-emphasis-and-hierarchy Enforces visual hierarchy rules in UI: button priorities, contrast, whitespace, and text emphasis. Useful when designing dashboards, cards, and CTA sections. CX Codex +1 874installs component-family-consistency Keeps buttons, inputs, badges and pills on the same radius, colour logic, shadow, border and spacing. Useful when building or reviewing a component library. CX Codex +1 874installs loading-states-and-perceived-performance A coding-agent skill that picks the right spinner, skeleton or progress bar and makes a slow interface feel faster. CX Codex +1 871installs generate-ui-from-brand Turns a site URL or a DESIGN.md file into design tokens, UX decisions and a UI spec you can paste into a project. CX Codex +1 870installs micro-interactions A skill for small purposeful animations: toggles, animated icons, success moments. Useful when an interface gives no feedback. CX Codex +1 869installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor