# A button has eight states

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

Module: Components and states · Level: beginner · 8 min · https://school.agents.org.in/design/components/button-states

Press the Save button. Then walk through the picker, one state at a time.

*[Interactive demo: One button, eight looks. The width never changes, so nothing around it moves.]*

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 hover state (How a control looks while a mouse pointer is over it. Good ones are subtle (one step of background, a 1px lift) and only appear on devices that can hover).
- **Pressed.** `scale(0.97)` on `:active`, through a ~150ms transition. Never below 0.95.
- **Focus.** A visible ring for keyboard users, through `:focus-visible` (A CSS selector that matches when the browser decides focus should be shown, after Tab but not after a mouse click. Use it to show a focus ring only to keyboard users). 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.

*(The loading label and the done label live in the same box, stacked. Swapping them never resizes the button.)*

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

*[Interactive demo: Real hover with a mouse; the switch shows it on a touch screen.]*

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

  **Step: Hold the weight.** Never change font weight on hover or selection; bold text is wider and reflows the row. Signal with color.

  **Step: Tabular numbers.** `font-variant-numeric: tabular-nums` on anything that counts, so digits stop jiggling.

  **Step: No layout shift.** Skeletons, images and placeholders carry the loaded size or an `aspect-ratio`.

  **Step: States that stay put.** Hover transitions only the properties that change, ~150ms ease-out. Focus rings stay neutral and never vanish.

  **Step: One z-index scale.** Dropdown 100, modal 200, tooltip 300, toast 400, or `isolation: isolate`. Never `z-index: 9999`.

  **Step: 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.

  **Step: Decoration is inert.** `pointer-events: none` on 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.

**Every state of a control (paste into your agent):**

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

**Check yourself:** A tab bar makes the selected tab bold. What goes wrong, and what is the fix?

- [ ] Nothing; bold is the clearest signal
- [x] Bold text is wider, so the tabs shift sideways; keep the weight and signal with color
- [ ] Bold is too slow to animate; use a 300ms transition
- [ ] Bold fails contrast; make it uppercase instead

*Why:* Changing weight reflows the text and shoves its neighbors. Hold the weight constant and show selection with color or an indicator.

## In the skill

- https://school.agents.org.in/skill/components/hover-states-subtle.md
- https://school.agents.org.in/skill/components/ui-polish-pass.md
- https://school.agents.org.in/skill/components/interaction-personality.md
