Checkbox
Examples
Sizes
Figma size sets: Sm 16px · Md 20px.
States
Interaction states from Figma: Default, Hover, Focus, Disabled, Read-only — across Unchecked, Checked, and Indeterminate.
Validation
Figma Validation=Invalid (Negative) — use validation="error". Unchecked uses a light negative fill; checked/indeterminate use solid negative.
Checkbox with Label
CheckboxOption is Figma Checkbox.Option — label required, optional description.
Option Group — Vertical
Option Group — Horizontal
Controlled
Group with Controlled Value
Selected: none
TanStack Form
Submit without accepting terms to see validation. Checking the box and submitting again succeeds.
RTL
Wrap the control (or a parent) in dir="rtl" / lang="fa". Option labels and descriptions align to the inline-start edge.
API Reference
Checkbox
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "sm" | Box size — Sm 16px / Md 20px |
validation | "none" | "error" | "none" | Figma Validation (error = Invalid Negative) |
checked | boolean | — | Controlled checked state |
defaultChecked | boolean | false | Initial checked state (uncontrolled) |
indeterminate | boolean | false | Mixed/indeterminate state |
disabled | boolean | false | Disables interaction |
readOnly | boolean | false | Prevents toggling (disabled surface) |
required | boolean | false | Marks as required for form validation |
name | string | — | Form field name |
value | string | — | Value submitted with the form |
onCheckedChange | (checked: boolean) => void | — | Called when toggled |
CheckboxOption
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | — | Label text (required) |
description | ReactNode | — | Helper text below the label |
All Checkbox props | — | — | Passed through to the inner Checkbox |
CheckboxOptionGroup
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "vertical" | "horizontal" | "vertical" | Layout direction |
value | string[] | — | Controlled selected values |
defaultValue | string[] | — | Initial values (uncontrolled) |
onValueChange | (value: string[]) => void | — | Called when selection changes |
allValues | string[] | — | All possible values (for parent checkbox) |
disabled | boolean | false | Disables all checkboxes in the group |
Accessibility
WCAG success criteria
| Criterion | Level | How EDS helps | |
|---|---|---|---|
| 2.1.1 Keyboard | A | Space and Enter toggle; Tab reaches every enabled control; disabled controls are inert | |
| 2.4.7 Focus Visible | AA | :focus-visible ring with 2px offset outside the control | |
| 1.3.1 Info and Relationships | A | CheckboxOption associates label + control; groups use role="group" with an accessible name | |
| 4.1.2 Name, Role, Value | A | Checkbox role, checked/mixed/disabled states, and form name/value exposed to assistive tech | |
| 1.4.3 Contrast (Minimum) | AA | Label and description text use default foreground tokens on muted surfaces | |
| 1.4.11 Non-text Contrast | AA | Box border, fill, check icon, and error styling meet 3:1 against adjacent colors | |
| 3.3.1 Error Identification | A | validation="error" styles the control; consumers render descriptive error text | |
| 3.3.2 Labels or Instructions | A | CheckboxOption requires a label; groups require aria-label / aria-labelledby | |
| 2.5.8 Target Size (Minimum) | AA | CheckboxOption label expands the clickable region; bare checkboxes need adequate surrounding spacing or an associated label |
Why it matters
Checkboxes gate consent, permissions, and multi-select filters — mistakes here block purchases, hide required settings, or silently change user preferences. Keyboard-only users, screen-reader users, and people with motor impairments rely on a predictable toggle, a visible focus indicator, and error feedback that does not depend on color alone. Indeterminate “select all” states must communicate partial selection clearly; otherwise bulk actions feel broken or untrustworthy.
Keyboard
| Key | Context | Behavior |
|---|---|---|
| ⇥ Tab / ⇧ + ⇥ Tab | Any checkbox | Moves focus to the next/previous focusable control in document order |
| Space | Focused, enabled checkbox | Toggles checked ↔ unchecked (Base UI checkbox) |
| ↵ Enter | Focused, enabled checkbox | Toggles checked ↔ unchecked (form submit contexts) |
| Space / ↵ Enter | disabled | No toggle — control is removed from the tab order when disabled |
| Space / ↵ Enter | readOnly | No toggle — control stays focusable but state does not change |
| ⇥ Tab | Inside CheckboxOptionGroup | Moves between individual checkboxes; each option is independently focusable |
Groups do not use arrow-key roving focus — unlike radios, multiple checkboxes in a group may all be checked, so each option keeps its own tab stop.
Screen readers & semantics
| Surface | Semantics | Notes |
|---|---|---|
Checkbox | Native role="checkbox" via Base UI | Exposes checked, mixed (indeterminate), and disabled states |
CheckboxOption | <label> wrapping control + text | Label text becomes the accessible name; no extra aria-label needed when label is visible |
Bare Checkbox | Requires aria-label, aria-labelledby, or visible label | Icon-only or standalone boxes must not ship unlabeled |
CheckboxOptionGroup | role="group" | Provide aria-label or aria-labelledby naming the set (e.g. “Notifications”) |
| Check / minus icons | aria-hidden="true" | Decorative — state is announced from the checkbox role |
validation="error" | Visual styling only | Pair with error text (role="alert") and aria-describedby pointing at the message |
indeterminate | mixed state | Used for parent “select all” rows when some children are checked |
Descriptions on CheckboxOption render as visible helper text; associate them with aria-describedby when they convey essential instructions, not just marketing copy.
Focus & visuals
- Focus ring:
:focus-visibledraws a 2px gap + 2px stroke outside the box via a::afterpseudo-element, using--eds-color-outline-primary(overridable with--eds-component-checkbox-focus-ring). Matches other EDS form controls. - Contrast: Checked and indeterminate fills use semantic tokens (
--eds-color-bg-primary,--eds-color-icon-neutral-inverse); error states use negative tokens so invalid fields remain distinguishable at 3:1 non-text contrast. - Target size: Sm (16px) and Md (20px) boxes sit inside clickable
<label>regions onCheckboxOption, expanding the hit area beyond the minimum 24×24px touch target where a label is present. - RTL: Layout uses logical properties — label and description align to
inline-start; no separate LTR-only API. - Reduced motion: State transitions respect
--eds-motion-duration-*; the checkbox does not rely on animation to convey state.
Build with it
Do
- Use
CheckboxOption(or an external<label htmlFor="…">) for every user-facing choice — never rely on proximity alone. - Name groups with
aria-labeloraria-labelledbyonCheckboxOptionGroup. - Wire validation:
validation="error"on the control + visible error copy withrole="alert"andaria-describedby. - Use
indeterminateonly for true partial-selection (“select all” with some children checked); clear it when all/none are selected. - Keep
readOnlyfor display-only checked states that must remain focusable for context.
Don't
- Ship bare
<Checkbox />withoutaria-labelor a visible label. - Use color alone to signal errors — always pair
validation="error"with text. - Disable a checkbox to mean “read-only” — use
readOnlyso assistive tech still announces the value. - Hide required checkboxes off-screen while keeping them in the tab order — use visible labels and instructions.
Quick test
- Keyboard-only (2 min): Tab to each checkbox in a group; toggle with Space and Enter; confirm
disabledboxes skip andreadOnlyboxes do not change. - VoiceOver / NVDA (2 min): Verify each option announces “checkbox, checked/unchecked/mixed, disabled”; confirm the group name is read when entering the set.
- Error path (1 min): Submit a TanStack Form without checking a required box — error text appears, control gets error styling, and the message is linked via
aria-describedbyor read after focus. - Zoom & contrast (1 min): At 200% zoom, focus ring remains outside the box; error and checked states are still distinguishable without color alone.
- RTL (30 sec): Set
dir="rtl"— label sits on the inline-start side and reading order feels natural.