Checkbox

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

AAWCAG 2.2 target
12/18contrast pairs passing
3keyboard interactions

The Checkbox is a native <input type="checkbox"> laid over the drawn box, so the browser supplies its role, its checked state and Space to toggle. The drawing is hidden from assistive tech. What the component has to get right is the name, the mixed state, and contrast for a box that is only 16px.

Keyboard#

Tab

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

Shift+Tab

Moves focus to the previous focusable element.

Space

Toggles the checkbox. From Unspecified, the first press checks it.

Semantics#

<input type="checkbox">

Native role, state and keyboard. It covers a 24px square over the 16px box, so the target is larger than the drawing.

Always
<label>

The row and the card are label elements, so a click anywhere on them toggles the box.

Label row and card
aria-labelledby

Points at the label text, so the box is announced by its label and not by the sub-label or badge.

Label row and card
aria-describedby

Links the description, so it is read after the label.

description
aria-checked="mixed"

Set alongside the input's indeterminate property, so it is announced as partially checked.

Unspecified
disabled

Removed from the tab order and announced as unavailable. Read Only uses it too.

Disabled or Read Only
<fieldset> + <legend>

Names the question the options answer, so each box is read in context: Notifications, Email, checkbox, checked.

A group

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.

3:1

Checked box and tick · Active, Unspecified

The filled box against the page, and the white tick or dash against the fill. Both are non-text, so 3:1 applies; every intent and state clears it comfortably.

WCAG 1.4.11 Non-text Contrast ↗
3:1

Unchecked box edge · Resting

The 1px #e1e1e1 stroke is the only thing that shows an empty box. It measures 1.31:1 on white, well under 3:1. The Shadow appearance's grey base is lower still.

WCAG 1.4.11 Non-text Contrast ↗
4.5:1

Label and description · Label row, Card

Label #2a2a2a and description #6d6d6d against white, and against a hovered card's 4% wash. Both pass.

WCAG 1.4.3 Contrast (Minimum) ↗
3:1

Focus indicator · Focus visible

A 2px ring in #1a6ef8 off a white gap, on keyboard focus only. It measures 4.54:1 on white, so it clears 3:1 on every light surface the library uses.

WCAG 1.4.11 Non-text Contrast ↗WCAG 2.4.13 Focus Appearance ↗
Not required

Disabled · Disabled

Inactive controls are exempt. The disabled text and glyphs (#cbcbcb) are deliberately faint.

WCAG 1.4.3 Contrast (Minimum) ↗

Measured contrast#

Each part against what sits directly behind it on a white page, from the ds.css tokens; text is held to 4.5:1 and the box, its edge and its tick, as graphics, to 3:1. 6 of 18 pairs fall below their minimum (4.5:1 for text, 3:1 for graphics) and are marked in amber.

Appearance · partRestHoverFocused
Neutral · tick on fill8.59passes14.35passes20.14passes
Brand · tick on fill5.40passes8.53passes13.54passes
Unchecked · box edge1.31below the minimum1.15below the minimum1.31below the minimum
Shadow, unchecked · base on page1.12below the minimum1.62below the minimum1.12below the minimum
Card · label14.35passes13.17passes14.35passes
Card · description5.17passes4.75passes5.17passes

Motion#

  • The box's fill and the tick fade over 120ms, and a card's wash over 150ms. Both are turned off under prefers-reduced-motion.
  • Nothing slides or scales.

Checklist#

Every checkbox has a visible label. A lone box, such as I accept the terms, still needs one.

Related checkboxes sit in a <fieldset> whose <legend> asks the question.

The drawn box is 16px but the input covers 24 × 24px, which meets WCAG 2.5.8. Keep the 20px frame; don't shrink it to the box.

The empty box's #e1e1e1 edge is 1.31:1 on white, below the 3:1 non-text minimum. Content/Muted (#8f8f8f, 3.23:1) would pass; it needs a darker stroke before the code library ships.

Unspecified is set from code, never by the person. Decide what a click on a mixed select-all does, and make the label say it if it is not obvious.

Read Only is drawn with disabled, so it leaves the tab order. Where the value matters, also state it in text.

A required checkbox has no error look of its own: put the reason in a Hint under it, linked with aria-describedby.