Components · Checkbox
Components

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 attributeel.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?

ControlUse whenTakes effectExample here
CheckboxZero or more of a set, or one opt-inOn submitFilter by category; accept the terms
SwitchOne setting, on or offImmediatelyEmail me when an organiser replies
RadioExactly one of two or moreOn submitWhich 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.

Categories to include

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

PropertyValueWhy
Box18px, --radius-sm, 1px --color-border-strongSame 3.06:1 boundary as every other control
Checked fill--color-cta with a white checkInk commits — the same fill as the primary button
Target44×44 minimum, delivered by label paddingThe box is 18px; the target must not be
LabelWraps the input, or for=Clicking the words must toggle the box
Gap10px between box and labelClose enough to bind, far enough to read
FocusThe one system focus ring, on the boxNever on the whole label row — it looks like a selection
Erroraria-invalid + aria-describedby + a messageA red box alone says nothing about what is wrong
IndeterminateA dash, never a check“Some” must not look like “all” at a glance

Do & don't

Do — describe what happens when it is ticked
Don't — a negative label makes ticked mean “no”
Do — one line, sentence case, no full stop
Don't — nobody read that, and the tick is now a lie

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: none the input. It leaves the tab order and stops being a form control. Clip it instead, or style it with appearance: none and 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-required and the visible required marking, 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}
/>
Sporting Scouter Design System
Every token on this site is generated from theme.css.