Button

How the Button works with a keyboard and assistive tech, and where it stands against WCAG 2.2 AA.

FreeReact and Vue
AAWCAG 2.2 target
29/54contrast pairs passing
22pxsmallest target height
4keyboard interactions

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#

Tab

Moves focus to the button. A disabled button is skipped.

Shift+Tab

Moves focus to the previous focusable element.

Enter

Activates the button.

Space

Activates the button when the key is released.

Semantics#

<button type="button">

Native role and keyboard handling. Inside a form, pass type="submit" deliberately.

Always
aria-label

Set from the label, so the button still has a name when no text shows.

iconOnly
aria-busy="true"

Announced as busy. The button is disabled while loading so it can't be pressed twice.

loading
disabled

Removed 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 loading
aria-hidden

Icons beside a label are decorative and hidden from assistive tech.

Icons

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

4.5:1

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) ↗
3:1

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 ↗
3:1 if needed

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 ↗
3:1

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 ↗
Not required

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.

Intent · variantRestHoverPressed
Neutral · Primary14.35passes8.59passes20.14passes
Neutral · Secondary5.17passes4.75passes12.04passes
Neutral · Tertiary14.35passes4.75passes12.04passes
Brand · Primary5.40passes8.53passes13.54passes
Brand · Secondary5.40passes4.81passes3.10below the minimum
Brand · Tertiary5.40passes4.81passes3.10below the minimum
Success · Primary1.94below the minimum3.68below the minimum7.62passes
Success · Secondary3.68below the minimum3.37below the minimum2.64below the minimum
Success · Tertiary3.68below the minimum3.56below the minimum3.16below the minimum
Warning · Primary2.27below the minimum4.00below the minimum7.91passes
Warning · Secondary4.00below the minimum3.55below the minimum2.45below the minimum
Warning · Tertiary4.00below the minimum3.86below the minimum3.22below the minimum
Error · Primary4.49below the minimum7.03passes11.60passes
Error · Secondary4.49below the minimum4.17below the minimum3.01below the minimum
Error · Tertiary4.49below the minimum4.17below the minimum3.01below the minimum
Inverse on dark · Primary14.35passes12.82passes8.85passes
Inverse on dark · Secondary14.35passes9.74passes6.58passes
Inverse on dark · Tertiary14.35passes9.74passes6.58passes

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.

XXS24/22spacing
XS32/28AA
S40/36AA
M48/44AAA
L60/52AAA
XL72/60AAA
Primary, Secondary Tertiary

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.