Forms & Controls
Checkbox (incl. tri-state)
A control allowing a binary or tri-state (checked/unchecked/mixed) choice.
Last verified against WCAG 2.2 and WAI-ARIA APG 1.2 on 2026-07-02.
Implementation
checkbox-pattern.tsx
function CheckboxBox({ state, label, onToggle }) {
// state: "true" | "false" | "mixed"
return (
<button
type="button"
role="checkbox"
aria-checked={state === "mixed" ? "mixed" : state === "true"}
onClick={onToggle}
onKeyDown={e => {
if (e.key === " ") { e.preventDefault(); onToggle(); }
}}
>
<span aria-hidden="true" className="box">
{state === "true" && <CheckIcon />}
{state === "mixed" && <DashIcon />}
</span>
{label}
</button>
);
}
// Parent state is always DERIVED from children, never stored separately:
const parentState = allChecked ? "true" : noneChecked ? "false" : "mixed";Live demo
Required roles, states & properties
| Element | Attribute | Why |
|---|---|---|
| Checkbox <button> | role="checkbox" | Overrides the button's default semantics so AT announces "checkbox" and reads the state below instead of a generic pressed/not-pressed toggle. |
| Checkbox <button> | aria-checked="true"|"false"|"mixed" | The literal string "mixed" is what lets a screen reader announce "partially checked" for a parent whose children are only partly selected — a plain boolean can't express that third state. |
| Checkbox <button> | Accessible name (visible text or aria-label) | Every checkbox needs a programmatically associated name so AT can announce what is being checked, per SC 4.1.2 and 2.5.3 Label in Name. |
| Native <input type="checkbox"> | .indeterminate (DOM property, not an attribute) | Sets the visual mixed dash and the accessible "mixed" exposure for a native checkbox; must be set via ref in an effect since JSX has no matching prop. |
Keyboard interaction model
| Key | Behavior |
|---|---|
| Tab / Shift+Tab | Moves focus to the next/previous checkbox in the group. |
| Space | Toggles the focused checkbox. Enter is intentionally not bound — checkboxes only respond to Space, unlike buttons or links. |
Focus management rules
- Every checkbox in a group is individually focusable via Tab — there is no roving tabindex for checkboxes (unlike radio groups).
- Toggling a checkbox never moves focus away from it.
- A parent "select all" checkbox is never removed from the tab order, even while showing a mixed state.
WCAG 2.2 success criteria mapping
| SC | Name | Level | Why it applies |
|---|---|---|---|
| 1.3.1 | Info and Relationships | A | Structure and relationships conveyed visually are also programmatically determinable. |
| 2.1.1 | Keyboard | A | All functionality is operable through a keyboard interface with no specific timing. |
| 2.5.8 | Target Size (Minimum) | AA | Pointer targets are at least 24x24 CSS pixels, unless spaced, inline, or essential. |
| 3.3.2 | Labels or Instructions | A | Labels or instructions are provided when content requires user input. |
| 4.1.2 | Name, Role, Value | A | For all UI components, name, role, and value are programmatically determinable; states and changes are announced. |