Forms & Input
Checkbox
AvailableAn on/off selection control. Also covers handling the indeterminate (mixed) state.
What is a Checkbox?
A checkbox is a UI control that lets users toggle an item "on" or "off." It is commonly used for enabling settings, agreeing to terms of service, and selecting multiple items (notification types, filter criteria, etc.).
A "select all" parent checkbox introduces an additional complexity: beyond "all on" and "all off," it must represent a third state (indeterminate / mixed) when only some children are checked. This is a frequently overlooked detail.
Why Does Accessibility Matter?
- Keyboard-only users. A native
<input type="checkbox">can be focused with Taband toggled with Space. A<div>-based control cannot be operated at all. - Screen reader users. When properly associated with a
<label>, the screen reader announces something like "Email, checkbox, not checked" — communicating the name, role, and state. Without that association, there is no way to know what the checkbox is for.
The shortest path is to use a native <input type="checkbox">associated with a <label>. For the mixed state, use the indeterminate property.
Live Demo (Recommended Implementation)
Below is a "Select all" checkbox with three child checkboxes. When only some children are checked, the parent enters theindeterminate state (neither fully checked nor empty in appearance).
Try it: Tab to each checkbox → Space to toggle on/off. Check only one child and the parent becomes indeterminate; check all children and the parent also becomes checked.
Tip
In the indeterminate state, screen readers announce the "Select all" checkbox as "partially checked / mixed." This happens because settingindeterminate = true causes the browser to automatically exposearia-checked="mixed" to assistive technologies.
Keyboard Interaction
| Key | Action | Priority |
|---|---|---|
| Tab | Move focus to the next checkbox | Required |
| Space | Toggle the focused checkbox on/off | Required |
Note
Toggling with Space is automatically provided simply by using <input type="checkbox">. There is no need to write custom key handling.
Required WAI-ARIA Roles and Properties
| Target | Attribute / Role | Meaning |
|---|---|---|
| Input element | <input type="checkbox"> | Native checkbox role, state, and keyboard interaction are all provided automatically. |
| Label | <label> (wrapping the input or associated via for) | Serves as the accessible name for the checkbox. Clicking the label also toggles the state. |
| Parent (select all) | indeterminate = true (JS property) | Represents the mixed (indeterminate) state when only some children are selected. Automatically conveyed as aria-checked="mixed". |
| Group | <fieldset> + <legend> | Groups related checkboxes and provides a group name (optional but recommended). |
Note
indeterminate is a JavaScript property, not an HTML attribute(el.indeterminate = true). Even when the visual appearance shows the mixed state, the underlying checked value remains true or false, so be careful when handling form submission values.
Implementation: Recommended Pattern (Good)
Good / Recommended
Use a native <input type="checkbox"> associated with a <label>, and represent the parent's mixed state with indeterminate.
Markup:
<fieldset>
<legend>Notifications to receive</legend>
<!-- Parent: select all (intermediate state expressed via indeterminate) -->
<label>
<input type="checkbox" id="cb-all"> Select all
</label>
<!-- Children: native input + label association (toggle with Space) -->
<label><input type="checkbox" class="cb-child"> Email</label>
<label><input type="checkbox" class="cb-child"> SMS</label>
<label><input type="checkbox" class="cb-child"> Push notifications</label>
</fieldset>Syncing "Select all" and the indeterminate state:
const all = document.getElementById('cb-all');
const children = document.querySelectorAll('.cb-child');
// Parent -> children: toggle all at once
all.addEventListener('change', () => {
children.forEach((c) => { c.checked = all.checked; });
all.indeterminate = false; // All are now in sync, so clear the mixed state
});
// Children -> parent: update parent based on all / some / none checked
children.forEach((c) => c.addEventListener('change', () => {
const checked = [...children].filter((x) => x.checked).length;
all.checked = checked === children.length; // all -> on
all.indeterminate = checked > 0 && checked < children.length; // some -> mixed
}));Anti-pattern (Bad)
Below is a "visual-only" checkbox built with <div> elements.It toggles with a mouse click, but cannot be operated with a keyboard, and its state is not announced.
Try it: pressing Tab does not move focus to the checkboxes. Screen readers do not announce them as 'checkbox' or convey their on/off state.
<!-- ❌ Anti-pattern: a "checkbox-like" control built with a div -->
<div class="checkbox" onclick="this.classList.toggle('on')">
<span class="box"></span> Email
</div>Bad / Avoid
Issues with this implementation:
- Not keyboard accessible — a
divcannot receive focus. - Role is not conveyed — it is not recognized as a "checkbox."
- State is not conveyed — the on/off (or indeterminate) state is not announced.
- No label association — clicking the text does not toggle the control, and no accessible name is provided.
Implementation Checklist
- Checkboxes use <input type="checkbox">
- Each checkbox is associated with a <label> (wrapping or for/id)
- Space toggles the state, and focus indicators are visible
- Partial selection in a "select all" pattern uses indeterminate = true
- Parent and child states stay synchronized on change events
- Related checkboxes are grouped with <fieldset> + <legend> (optional but recommended)
Source (English):Checkbox Pattern — W3C APG(opens in a new tab)