Checkbox
Turns one option on or off, or a whole set at once.
Examples
Checked
falsedefault
true
indeterminate
States
rest
hover
pressed
focus-visible
ticked
mixed
invalid
disabled
read-only
Usage
When to use it
- Use
- A form asks for a yes or no that applies when the form is saved.
- Use
- The person picks any number from a short visible list, including none.
- Use
- Table rows are selected for a bulk action, with a select-all in the head.
- Not for
- The setting applies the moment it changes. Use Switch.
- Not for
- Exactly one option can be chosen. Use Radio group.
- Not for
- More than eight options, or a long searchable list. Use Combobox.
Rules
8 rules
- Must
- Build select-all as <Checkbox parent/> inside a CheckboxGroup with allValues; never compute mixed by hand. Because a hand toggle from mixed clears or fills everything, while parent cycles back to the set the person picked.
- Must
- Wrap the checkbox and its text in one
<label>, or use Field. Because a loose span is not the accessible name and not a click target, so a screen reader hears "checkbox" and only the 16px box toggles. - Must
- Set aria-invalid after a submit attempt, never on first render. Because aria-invalid turns the edge to the fail line and announces invalid before the person has done anything wrong.
- Should
- State what ticking does, in the positive; no "Don't" labels. Because ticking a negative label makes the person undo a double negative to know what happens.
- Must
- Show mixed with the minus glyph, never a lighter or partial fill. Because a lighter fill reads as disabled, and forced colors drop it so mixed looks unticked.
- Never
- Color a ticked checkbox; it stays ink. Because hue names a kind or a status in Quartz, so a colored tick reads as a warning or an agent setting.
- Should
- Switch to a multiple Combobox past 8 options. Because a long list of boxes has no search and pushes the rest of the form off the screen.
- Should
- Use items-start on a row whose label can wrap. Because centered, the box drifts to the middle of a two-line label instead of its first line.
Accessibility
6 checks
- The box is role="checkbox" with aria-checked true, false or mixed, named by the wrapping label.
- Space toggles; a parent cycles all, none, then the old mix.
- The glyph is aria-hidden; mixed reads without color through the minus.
- A group is role="group" named by aria-labelledby.
- The unticked edge is --border-control, 3:1 on paper, and the ticked fill carries the state besides.
- The box is 16px with a 28px hit area; the label makes the whole line the target.
Edge cases
11 edge cases
- Long wrapping label
- Use items-start so the box aligns to the first line.
- No label
- Announced as "checkbox" with no name; pass aria-label.
- Select-all over 500 values
- The parent reads the value array, not rendered boxes.
- Right to left
- The box moves to the start with the label.
- Zoom 200%
- The box stays 16px, centred on the first line.
- Invalid after submit
- The fail line on the edge and the error text it describes; ticking clears it.
- Inside a clickable row
- The box stops its click from bubbling.
- Double click on the label
- Toggles twice and ends where it began.
- Space held down
- Toggles once, on key up.
- Touch
- Tapping the label toggles; the tap highlight is off.
- Forced colors
- CanvasText border; ticked and mixed use Highlight with HighlightText; disabled is GrayText.
API
6 props
valuestringparentbooleandefaultfalseindeterminatebooleandefaultfalsearia-invalidbooleandefaultfalsedisabled, readOnly, required, nameBase UICheckboxGroup value, onValueChange, allValuesstring[]
Tokens
12 tokens
--surface-page- var(--paper)
--border-control- oklch(var(--ink-c) / 0.50)
--fail-line- oklch(60% 0.236 26)
--radius-soft- 6px
--ink- oklch(var(--ink-c))
--text-inverse- var(--paper)
--surface-pressed- oklch(var(--ink-c) / 0.08)
--text-disabled- oklch(var(--ink-c) / 0.40)
--line-focus- 2px
--border-focus- var(--ink)
--focus-offset- 2px
--ease-step- cubic-bezier(0.2, 0, 0, 1)