Checkbox
A native <input type="checkbox">, restyled. Not a div with role="checkbox" — the native control brings keyboard behaviour, form participation, autofill, and the platform's own high-contrast rendering, and hand-rolled versions reimplement three of those badly and forget the fourth.
States
You need to accept this to continue.
Indeterminate is a property, not an attribute — el.indeterminate = true in JS. There is no indeterminate attribute in HTML, and markup that pretends otherwise renders unchecked with no warning.
Checkbox, switch or radio?
| Control | Use when | Takes effect | Example here |
|---|---|---|---|
| Checkbox | Zero or more of a set, or one opt-in | On submit | Filter by category; accept the terms |
| Switch | One setting, on or off | Immediately | Email me when an organiser replies |
| Radio | Exactly one of two or more | On submit | Which distance did you run? |
The dividing line is when the change lands. A checkbox that saves the instant you tick it should have been a switch; a switch inside a form with a Save button should have been a checkbox. Getting this wrong is why people press Save and wonder whether it worked.
Groups
A set of related checkboxes is a <fieldset> with a <legend>. Without it, a screen reader announces seven checkboxes with no idea what they have in common.
2 of 7 selected
The parent is the only legitimate use of indeterminate: it reflects its children and is never a third state the user can choose. Clicking it selects all or clears all — from indeterminate, it selects all, because “some” to “all” is the move people expect.
Specification
| Property | Value | Why |
|---|---|---|
| Box | 18px, --radius-sm, 1px --color-border-strong | Same 3.06:1 boundary as every other control |
| Checked fill | --color-cta with a white check | Ink commits — the same fill as the primary button |
| Target | 44×44 minimum, delivered by label padding | The box is 18px; the target must not be |
| Label | Wraps the input, or for= | Clicking the words must toggle the box |
| Gap | 10px between box and label | Close enough to bind, far enough to read |
| Focus | The one system focus ring, on the box | Never on the whole label row — it looks like a selection |
| Error | aria-invalid + aria-describedby + a message | A red box alone says nothing about what is wrong |
| Indeterminate | A dash, never a check | “Some” must not look like “all” at a glance |
Do & don't
Structure
<fieldset>
<legend>Categories to include</legend>
<label class="checkbox">
<input type="checkbox" name="cat" value="communication" checked>
<span>Communication</span>
</label>
<!-- Error: the message is referenced, not just adjacent -->
<label class="checkbox checkbox--error">
<input type="checkbox" name="terms" aria-invalid="true" aria-describedby="terms-err">
<span>I confirm this event took place</span>
</label>
<p id="terms-err" class="field__error">You need to accept this to continue.</p>
</fieldset>
<script>
// Indeterminate has no HTML attribute — it exists only as a DOM property.
parent.indeterminate = someChecked && !allChecked;
</script>Rules
- Never
display: nonethe input. It leaves the tab order and stops being a form control. Clip it instead, or style it withappearance: noneand keep it in place. - Label affirmatively. Ticked always means yes. A checkbox whose label contains “don't”, “no” or “without” makes the checked state mean the opposite of checked.
- No colour-only state. Checked is a check mark plus a fill; indeterminate is a dash plus a fill. Both survive greyscale.
- Do not validate on change. An error the moment someone unticks a box they only just ticked is punishment, not help. Validate on submit — see Input.
- Required checkboxes get
aria-requiredand the visiblerequiredmarking, exactly like every other field.
React
From @sportingscouter/ui-react. The component adds no visual values of its own — every class comes from the same tv() recipe the design system defines, so the two cannot drift.
import {
Checkbox, ChoiceGroup, Switch,
} from '@sportingscouter/ui-react';
<ChoiceGroup legend="Categories to include">
<Checkbox label="Water on course" defaultChecked />
<Checkbox label="Signage" hint="Route and distance markers" />
<Checkbox label="Medal" disabled />
</ChoiceGroup>
{/* indeterminate is a DOM PROPERTY, not an attribute —
<input indeterminate> does nothing. The component sets it
via a ref effect for you. */}
<Checkbox
label="All categories"
indeterminate={some && !all}
checked={all}
/>
{/* Switch is for a setting that applies IMMEDIATELY. If the
change needs a Save button it is a Checkbox. That is the
whole rule. */}
<Switch
label="Email me new reviews"
checked={subscribed}
onChange={toggle}
/>theme.css.