Button
How the Button works with a keyboard and assistive tech, and where it stands against WCAG 2.2 AA.
The Button renders a native <button>, so the browser supplies its role, focus and keyboard behaviour. What is left to the component is keeping that intact through every variant, and meeting WCAG 2.2 AA for contrast, focus and target size.
Keyboard#
Moves focus to the button. A disabled button is skipped.
Moves focus to the previous focusable element.
Activates the button.
Activates the button when the key is released.
Semantics#
<button type="button">Native role and keyboard handling. Inside a form, pass type="submit" deliberately.
Alwaysaria-labelSet from the label, so the button still has a name when no text shows.
iconOnlyaria-busy="true"Announced as busy. The button is disabled while loading so it can't be pressed twice.
loadingdisabledRemoved from the tab order and not announced as actionable. If users need to find out why an action is unavailable, keep it enabled and explain on press instead.
disabled or loadingaria-hiddenIcons beside a label are decorative and hidden from assistive tech.
IconsColour contrast#
Every variant and state has to meet WCAG 2.2 Level AA. Measure the colours as they land on screen: a translucent hover or press wash over whatever sits behind it.
Text label · Rest, Hover, Pressed
Label text against the button's fill in every state. The label is 10–18px bold, below the large-text threshold, so 4.5:1 applies at every size. Measure a translucent hover or press over the surface behind it.
WCAG 1.4.3 Contrast (Minimum) ↗Icon-only glyph and spinner · Icon only, Loading
An icon that stands in for text, and the spinner's arc, need 3:1 against the fill. An icon beside a visible label does not need its own check.
WCAG 1.4.11 Non-text Contrast ↗Visible boundary · Secondary
The stroke only needs 3:1 where it is the only thing that shows there's a control. A button with a readable label can rely on the label.
WCAG 1.4.11 Non-text Contrast ↗Focus indicator · Focus visible
A 2px ring sits 2px off the edge with the page colour between, so it contrasts with the page, not the fill. It takes the intent's own colour, darkened where needed to stay at 3:1 or more on white: neutral #2a2a2a, brand #005cf8, success #229a14, warning #be6a00, error #ec0d33; Inverse on dark rings white.
WCAG 1.4.11 Non-text Contrast ↗WCAG 2.4.13 Focus Appearance ↗Disabled · Disabled
Inactive controls are exempt from contrast requirements. The disabled label (#cbcbcb) is deliberately faint.
WCAG 1.4.3 Contrast (Minimum) ↗Measured contrast#
Label against fill, for every intent, variant and state, measured from the component's colour tokens. Secondary and Tertiary are measured on white, Inverse on dark on #2a2a2a. 25 of 54 pairs fall below their minimum (4.5:1 for text, 3:1 for graphics) and are marked in amber.
Target size#
WCAG 2.5.8 (AA) asks for targets of at least 24 × 24px, or enough space around a smaller one. 44px (2.5.5, AAA) is the comfortable size for touch. Width grows with the label, so height is the measure that matters; icon-only buttons are square.
Motion#
- Colour changes between states take 150ms and are turned off under prefers-reduced-motion.
- The loading spinner only spins when motion is allowed. Under reduced motion it stays still, and aria-busy still says the button is working.
Checklist#
Every button has a name: visible text, or an aria-label on an icon-only button.
The label says what happens (Save changes, Delete project), not OK or Click here.
Use a link, not a button, when the only effect is going to another page.
Pairs the table marks below 4.5:1 — Success and Warning in most states, Error at rest, and the pressed washes of Secondary and Tertiary — need a darker shade before the code library ships.
A Tertiary XXS button is 22px tall, under the 24px minimum, so it passes only on spacing: keep 24px between the centres of neighbouring targets.
