BetaComponent · since 0.4.0
A native checkbox with its label: pick any number of options, or confirm one statement.
Pick this part
Pick c-checkbox for
- Choosing any number of options from a short list, including none.
- Confirming one statement before submitting, such as agreeing to terms.
- A setting that is saved when the form is submitted, not straight away.
Build it
- Start with
<div>and addclass="c-checkbox". - Add the required parts:
c-checkbox__input,c-checkbox__label. - Add the parts you need:
c-checkbox__hint. - Set state on the native control, never with data-state:
:checked,:indeterminate,:disabled,[aria-invalid="true"].
Show markup 19 lines
<fieldset class="c-form-field">
<legend class="c-form-field__label">Which updates do you want? (optional)</legend>
<div class="l-stack l-stack--sm">
<div class="c-checkbox">
<input class="c-checkbox__input" type="checkbox" id="updates-releases" name="updates" value="releases" checked>
<label class="c-checkbox__label" for="updates-releases">New releases</label>
</div>
<div class="c-checkbox">
<input class="c-checkbox__input" type="checkbox" id="updates-security" name="updates" value="security" aria-describedby="updates-security-hint">
<label class="c-checkbox__label" for="updates-security">Security notices</label>
<p class="c-checkbox__hint" id="updates-security-hint">Sent within a day of a fix.</p>
</div>
<div class="c-checkbox">
<input class="c-checkbox__input" type="checkbox" id="updates-digest" name="updates" value="digest" disabled aria-describedby="updates-digest-hint">
<label class="c-checkbox__label" for="updates-digest">Monthly digest</label>
<p class="c-checkbox__hint" id="updates-digest-hint">Coming in March 2027.</p>
</div>
</div>
</fieldset>
Check your build
Each piece that doesn't fit was run through mcss-lite validate when this page was built. The output below is real.
Fits
Group related checkboxes in a fieldset with a legend that asks the question.
Show markup 13 lines
<fieldset class="c-form-field">
<legend class="c-form-field__label">Which updates do you want?</legend>
<div class="l-stack l-stack--sm">
<div class="c-checkbox">
<input class="c-checkbox__input" type="checkbox" id="news-releases" name="news" value="releases" checked>
<label class="c-checkbox__label" for="news-releases">New releases</label>
</div>
<div class="c-checkbox">
<input class="c-checkbox__input" type="checkbox" id="news-security" name="news" value="security">
<label class="c-checkbox__label" for="news-security">Security notices</label>
</div>
</div>
</fieldset>
validate: 0 issues
Doesn't fit
Don't fake the checked state with data-state: it isn't submitted and screen readers never hear it.
<div class="c-checkbox" data-state="checked">
<input class="c-checkbox__input" type="checkbox" id="terms-fake">
<label class="c-checkbox__label" for="terms-fake">I agree to the terms</label>
</div>
validate
- error "checked" is a native state of c-checkbox: don't use data-state. Use the checked attribute, never data-state.
[invalid-state]
Fits
Show an unticked agreement as an error with aria-invalid and a linked message.
Agree to the terms of service to create an account.
Show markup 7 lines
<div class="c-form-field">
<div class="c-checkbox">
<input class="c-checkbox__input" type="checkbox" id="terms" aria-invalid="true" aria-describedby="terms-error">
<label class="c-checkbox__label" for="terms">I agree to the terms of service</label>
</div>
<p class="c-form-field__error" id="terms-error">Agree to the terms of service to create an account.</p>
</div>
validate: 0 issues
Doesn't fit
Don't leave a checkbox without its label: "on" and "off" mean nothing without the statement they answer.
<div class="c-checkbox">
<input class="c-checkbox__input" type="checkbox" id="news-alone">
<p class="c-checkbox__hint">Weekly, no ads.</p>
</div>
validate
- warning This <input> has no label. Add <label for="news-alone"> (c-form-field__label).
[label-missing]
Spec sheet
Apply to <div>
| Class | On | What it does |
|---|---|---|
c-checkbox__input required | <input> | The native control. Carries every state: checked, disabled, aria-invalid. |
c-checkbox__label required | <label> | The visible label, linked with for/id. Clicking it toggles the control and pads the hit area. |
c-checkbox__hint | <p> <div> <span> | Optional one-line hint under the label, linked with aria-describedby. |
| State | Meaning | Pair it with |
|---|---|---|
:checked | Selected: the box seats and fills with ink, with a paper tick. | Use the checked attribute, never data-state. |
:indeterminate | Mixed: some, not all, of the options below it are selected. Drawn as a flat bar. | Set input.indeterminate = true from JavaScript; HTML has no attribute for it. It is a display state, never a submitted value. |
:disabled | Cannot be changed. Dashed keyline, ghosted label. | Use the disabled attribute, and say nearby why it is unavailable. |
[aria-invalid="true"] | Needs attention, e.g. an unticked agreement on submit. Red keyline with a heavier bottom edge. | Set aria-invalid="true" and aria-describedby pointing at the c-form-field__error message. |
Show tokens 6
| Token | Value (light / dark) |
|---|---|
--checkbox-size | 1.5rem |
--checkbox-background | #ffffff / #14294d |
--checkbox-border | #1a1d20 / #a9bacb |
--checkbox-checked-background | #1a1d20 / #e6edf4 |
--checkbox-mark | #ffffff / #0f2140 |
--checkbox-radius | 0.25rem |
Accessibility
- Keep the native <input type="checkbox">: it brings the role, the checked state, Space to toggle and form submission for free.
- Every checkbox needs a <label for> on
c-checkbox__label; a group needs fieldset + legend. - The box is 24px and the label extends the hit area; never hide the input with display: none.
- aria-invalid is allowed on a checkbox; on a group, put data-state="error" on the fieldset instead.
| Key | What it does |
|---|---|
| Tab | Moves focus to the next checkbox. |
| Space | Ticks or clears the focused checkbox. |
Wording
- Labels are positive statements in sentence case, with no full stop.
- The legend asks the question; the labels are the answers.
- Mark an optional group "(optional)" in the legend.