Checkbox
A box you tick. Each option is independent — any number of them, including none.
Which one
| Control | Answers | Takes effect |
|---|---|---|
| Checkbox | Any number, including none | On Save |
| Radio | Exactly one, all options visible | On Save |
| Select | Exactly one, from a longer list | On Save |
| Switch | On or off | Immediately |
The group
A checkbox almost never appears alone. The fieldset carries the question; each label carries one answer.
<fieldset class="sr-checkbox-group">
<legend class="sr-checkbox-group__legend">Case note types</legend>
<p class="sr-checkbox-group__hint">Select all that apply.</p>
<div class="sr-checkbox-group__options">
<div class="sr-checkbox">
<input class="sr-checkbox__input" type="checkbox" id="n1" name="notes" checked>
<label class="sr-checkbox__label" for="n1">General notes</label>
</div>
</div>
</fieldset>
States
Indeterminate is a parent state — the "Select all" box when only some children are ticked. It is never something a user can choose directly.
<!-- Rest, hover and focus come from the stylesheet. Only these four
need markup. Indeterminate is the exception: it is a DOM property,
not an attribute, so it cannot be set in HTML alone. -->
<div class="sr-checkbox">
<input class="sr-checkbox__input" type="checkbox" id="c1" checked>
<label class="sr-checkbox__label" for="c1">Checked</label>
</div>
<div class="sr-checkbox sr-checkbox--error">
<input class="sr-checkbox__input" type="checkbox" id="c2">
<label class="sr-checkbox__label" for="c2">Error</label>
</div>
<div class="sr-checkbox">
<input class="sr-checkbox__input" type="checkbox" id="c3" disabled>
<label class="sr-checkbox__label" for="c3">Disabled</label>
</div>
<script>
// Indeterminate, set the only way it can be set.
document.getElementById('c4').indeterminate = true;
</script>
Error
One message for the whole group, above the options, with a red rule down the left. Never a message per option.
Long labels
A label that runs past the line wraps under itself, not under the box. The label is an inline-block with left padding rather than a flex sibling, which is what keeps the second line aligned with the first. Do not switch it to flex to "fix" the indent.
A box you tick. Use it when each option is independent, "any of these, including none".
When to use
- Options that are independent of one another. Ticking one says nothing about the others, and ticking none is a valid answer.
- A single yes/no that is part of a form and takes effect on Save, "I confirm the details are correct".
- Selecting rows in a table, with a "Select all" checkbox above them.
When not to use
- Exactly one answer required. Use a Radio group, a radio group cannot be un-answered by accident, a checkbox list can.
- Immediate effect, no Save. Use a Switch. A checkbox in a form promises "nothing happens until you save"; a switch promises the opposite.
- More than about ten options. Use a multi-select, past ten, a list stops being scannable and becomes a wall.
- As a way to show status. A checkbox is an input. If the user cannot change it, use a Tag or plain text.
How it works
- 20px box, 28px label offset. The same measurements as Radio, so a form mixing the two lines up.
- Group anatomy.
fieldset→ legend → hint → error → options. The legend names the question; each label names one answer. Both are needed, a legend of "Options" and labels of "Yes"/"No" tells a screen-reader user nothing. - Vertical by default. Horizontal is only for two or three short options, and it wraps rather than overflows.
- Long labels wrap under the label, not under the box. The label is
inline-blockwith left padding, not a flex sibling, so the second line aligns with the first rather than sliding under the tick. Do not "fix" this by switching the label to flex. - Indeterminate is a parent state, not a third answer. A "Select all" checkbox is indeterminate when some but not all children are ticked. Never offer indeterminate as something the user can choose.
- Error sits on the group, not on each option. One message above the options, with a red rule down the left of the whole group.
- Required shows an asterisk after the legend. The asterisk is decorative; the requirement is carried programmatically.
Do & don't
| Do | Don't |
|---|---|
| Write labels as the answer, "Include discharged patients" | Write labels as the question, "Discharged?" |
| Put the most common option first | Order options alphabetically when frequency is known |
| Use one group per question | Put two questions in one fieldset |
| Let a long label wrap | Truncate a label with an ellipsis |
| Keep "Select all" adjacent to the list it selects | Use indeterminate as a user-selectable state |
Accessibility
- Native
<input type="checkbox">. Nothing here is adivwith a click handler, so Space, form submission and assistive-technology support are the browser's, not ours. - The legend is the group's accessible name. Hidden legends stay in the markup
, hideLegend hides them visually only.
- The tick is not the only signal: the box fills and the border darkens, so the state survives greyscale.
- Hint and error are referenced from the group, so they are announced with it rather than as loose text nearby.
- Target size: the label is part of the target, so a 20px box still gives a row comfortably over 24px. Touch layouts should give each option a 44px row.
- Disabled uses the native attribute, so the option leaves the tab order and is omitted from submission, not merely greyed.
Known gaps
- No Blazor implementation yet; the CSS contract is stable enough to wrap.
- Touch-target sizing is guidance rather than a token, there is no
control.row.touchheight in/foundations/tokens/.
Accessibility requirements
| Requirement | WCAG SC | How Single Record meets it | Test method |
|---|---|---|---|
| Group name is announced with each option | 1.3.1, 4.1.2 | A real fieldset and legend, so "Case note types, General notes, checkbox" is announced rather than a bare label. hideLegend hides it visually only. | Screen reader announce |
| Each option has a programmatic label | 1.3.1 | A real label element bound to the input, so clicking the text toggles the box and the name reaches assistive technology. | Screen reader, click the label |
| Mixed state is exposed | 4.1.2 | indeterminate is set as a DOM property, which the browser exposes as aria-checked="mixed". | Screen reader, partial select-all |
| State is not colour alone | 1.4.1 | The box fills and gains a tick; the border darkens. All three change together, so the state survives greyscale. | Greyscale review |
| Error is exposed and actionable | 3.3.1, 3.3.3 | The message is referenced from the group and says what to do ("Select at least one note type"), not what went wrong. | Screen reader, submit empty |
| Required state is more than an asterisk | 3.3.2 | The asterisk is decorative; the group carries aria-required. | Screen reader announce |
| Target size | 2.5.8 | The 20px box relies on the spacing exception: 12px between options puts centres 32px apart. The clickable label extends the target further. Tightening the gap breaks this. | Measure spacing |
| Keyboard operable | 2.1.1 | Native input: Tab between options, Space to toggle. No custom key handling. | Keyboard only |
| Focus visible | 2.4.7 | A 3px Cyan/700 ring on the box alone, so it is not stretched around a long label. | Keyboard tab |