A button is a contract. In this lesson we map every state it can enter, then decide which ones deserve a visual difference.
Video lesson: One button, nine states. 16:24 long. A full transcript is available below.
Focus must survive on any background. Test it on an image, not just on white.
A button is not a rectangle with a label. It is a small contract: what the system promises at rest, under the pointer, under the keyboard, and when the work is not done yet.
Key idea
Every state needs a job. If two states look the same, one of them is decoration.
Nine states, each answering a different question. The signal is the smallest change that makes the answer legible without relying on color alone.
| State | Question | Signal |
|---|---|---|
| Rest | Is this actionable right now? | Surface + label |
| Hover | Is the pointer over it? | Tone shift |
| Focus | Where is the keyboard? | 2px ring + offset |
| Pressed | Did the press register? | 1px shift |
| Disabled | Why can't I use this? | Reduced contrast + reason |
| Loading | Is the work happening? | Reserved width + progress |
| Success | Did it work? | Confirmation, then rest |
| Danger | Can this be undone? | Destructive tone + confirm |
| Quiet | Is this secondary? | Border, no fill |
Name the action
Primary, secondary or destructive. The color is the last decision, not the first.
Design focus first
A keyboard user should never have to guess where they are. Build the ring before the hover.
Reserve space for async
Loading must not move the layout. Keep the label width and swap only the leading icon.
Test the edge
Disabled, error and success are states, not afterthoughts. Write the copy before the styles.
:root {
--action-bg: var(--surface-900);
--action-fg: var(--surface-0);
--action-bg-hover: var(--surface-800);
--action-ring: var(--focus-ring);
}
.button[data-loading="true"] {
cursor: progress;
color: transparent;
background-image: var(--action-bg);
}Exercise · 15 min
Audit one button in your product. List every state it can enter, then delete the ones that do not change the user's next move.