Button
One filled action per surface. Primary is ink on signal.
Spec
- Primary:
ink-on-signal—--action-fgon--signal. - Secondary: hairline.
- Ghost: no fill, no hairline until hover.
- Danger:
fg-criticaltext, not a vivid fill. The one filled action stays primary. - Visual height sm 28 / md 32 / lg 36 (
--control-h-*). Hit ≥44 via expanded target. - Icon + label, or icon-only with
aria-label. - Hover 120ms. Active
scale(0.97). - Loading: spinner replaces the icon, width locked,
aria-busy="true". - Disabled:
fg-disabled. Not a faded primary fill. hrefrenders<a>. Otherwise<button>.- Focus: 2px
--focus, 2px offset.
Live demo
Runnable controls. Not a screenshot. Paper / Ink in the header recolors tokens.
One filled action — primary is ink on signal
sm / md / lg visual, 44 hit
Loading (click), icon-only, href as a, disabled
Do / Don't
| Do | Don't |
|---|---|
| One primary per surface. | Two filled buttons in one footer. |
| Verb, one to three words. | OK, Submit, Click here. |
| Icon-only named with aria-label. | Icon-only with a tooltip as the only name. |
Motion
| Property | Duration | Easing | Trigger | Reduced motion |
|---|---|---|---|---|
transform: scale(0.97) | 120ms --dur-fast | --ease-out | :active | instant |
| color / border | 120ms --dur-fast | --ease-out | :hover | instant |
| outline | 80ms --dur-micro | --ease-out | :focus-visible | instant |
spinner rotate | 700ms linear loop | --ease-linear | aria-busy | static ring |
Accessibility
- Contrast: ink on signal is the primary pair. White on signal fails body text. See Color.
- Hit ≥44px even when the face is 28.
- Keyboard: native button or link. Disabled buttons stay in tab order only if the disabled reason must be read — prefer omit from the path.
- Loading:
aria-busy, name unchanged, width locked so the layout does not jump. - These notes describe construction. They are not a guarantee of WCAG conformance. Validate with axe, keyboard, and operators. See Accessibility.