A button has eight states
beginner·8 min·Markdown for agents
What this lesson teaches
Give every control its hover, pressed, focus, loading, done, error and disabled states, keep them from shifting the layout, and run the polish pass that makes a component feel finished.
Press the Save button. Then walk through the picker, one state at a time.
Resting: a clear label that starts with a verb. Press it to play the real sequence.
An agent will happily give you the resting button and a hover color. The other six states are where a button earns trust: it answers your press, tells you it is working, and confirms it worked, all in the same spot.
The eight, in one line each
- Rest. A label that starts with a verb: "Save changes".
- Hover. One step of background. Only on devices that can hover. This is the .
- Pressed.
scale(0.97)on:active, through a ~150ms transition. Never below 0.95. - Focus. A visible ring for keyboard users, through . More in the keyboard lesson.
- Loading. The label says what is happening ("Saving…"), clicks are ignored, the width holds.
- Done. It confirms on itself ("Saved"), holds a beat, returns to rest.
- Error. It says what did not happen; the details sit beside the form.
- Disabled. Faded and inert, with the reason written next to it.
Hover is not lift and shadow
The reflex hover is "lift the card 4px and add a big shadow". It is dated, and it is one of the tells of generated UI. Hover over both cards:
Hover over each card with a mouse. The left one jumps at you; the right one is felt more than seen, which is what a card you pass fifty times a day should do.
Quieter patterns read as more considered:
| Element | Hover |
|---|---|
| Rows, list items, table cells | A background shift, about rgba(0,0,0,0.04), over 120ms |
| Cards | translateY(-1px) and one step of tint |
| Inputs | The border gets one step darker |
| Icon buttons | The icon inside scales to 1.05, not the button |
A 1px lift is felt more than seen. 4px looks bouncy.
Two rules keep hover honest. First, gate it: wrap hover styles in @media (hover: hover) and (pointer: fine). A phone fires :hover on tap and leaves it stuck, which looks broken. Second, never change layout on hover: no margin, padding, width or font weight. Animate only transform, opacity, background-color, border-color or box-shadow, so the neighbors stay put.
Pressed, by product
How much a button squishes depends on who presses it. A consumer app can press to scale(0.96); it reads as delight. A tool someone uses all day stays subtler, scale(0.98) or just a background change, because a springy button pressed four hundred times a day gets tiring.
Disabled, layered right
opacity: 0.5 and cursor: not-allowed is fine for a simple button. On a text input, give it an explicit disabled background and muted text instead; a flat opacity on a complex element looks washed out. And the classic mistake: a submit button disabled until the form is valid tells the user nothing about why. Let them press it and show the errors.
The polish pass
Once the component works, run one pass over it. Each item is invisible on its own; together they are the difference between "works" and "finished".
Hold the weight
Never change font weight on hover or selection; bold text is wider and reflows the row. Signal with color.Tabular numbers
font-variant-numeric: tabular-numson anything that counts, so digits stop jiggling.No layout shift
Skeletons, images and placeholders carry the loaded size or anaspect-ratio.States that stay put
Hover transitions only the properties that change, ~150ms ease-out. Focus rings stay neutral and never vanish.One z-index scale
Dropdown 100, modal 200, tooltip 300, toast 400, orisolation: isolate. Neverz-index: 9999.Hit areas and optics
Targets of 44px on touch, grown with a pseudo-element. Nudge a play triangle 1 to 2px toward its point so it looks centred.Decoration is inert
pointer-events: noneon glows and ornaments, so they never swallow a click.
Polish applied to the wrong structure gets thrown away with it. If the hierarchy or spacing is wrong, fix that first.
Personality comes last, and sparingly
Once every state is right, a product can earn a little character in how it responds. Personality lives in interactions, not in static screens: a success message that names what happened ("Sent to 47 recipients" instead of "Success!"), a loading line that informs ("Fetching your last 30 days…"), a Delete that answers 100ms slower than Save because deliberation suits destruction.
Easter eggs belong where people are curious (a 404 page, Settings, a long press on the logo), never in the middle of signup or payment. And restraint wins: one well-placed surprise feels alive; ten feel exhausting. In a tool analysts use all day, skip it.
Give this button all of its states: rest, hover, pressed, focus, loading, done, error, disabled. - Hover only inside @media (hover: hover) and (pointer: fine); one step of background, no lift, no layout change. - Pressed: scale(0.97) on :active, 150ms ease-out. - Focus: :focus-visible ring, 2px, neutral color, 2px offset. - Loading: label "Saving…", ignore repeat clicks, fixed width; the labels are stacked layers that crossfade. - Done: "Saved" on the button for ~1.5s, then back to rest. Error: "Not saved", details beside the form. Then run a polish pass: constant font weight, tabular-nums on numbers, no layout shift, no z-index above the scale.
Quick check
A tab bar makes the selected tab bold. What goes wrong, and what is the fix?
In the skill
The nodes your agent reads when it does this work.
- Hover states — subtle by defaultHover is not lift-and-shadow. 1px shifts, color shifts, layered disabled state.
components/hover-states-subtle - The UI polish passThe invisible finish, as a pass — antialiasing, constant weight on interaction, tabular numbers, no layout shift, real characters, states that stay put, a z-index scale, hit areas, optical alignment, native page scrollbars, designed empty states, inert decoration, no flash on refresh.
components/ui-polish-pass - Interaction personalityUnique microinteractions, easter eggs, contextual feedback, sound. Personality lives in how the product responds, not how it looks.
components/interaction-personality
Connected lessons
What this lesson builds on and leads to, across every course.