Form

Checkbox

Checkboxes capture an independent on/off setting, or a partially-selected group.

Components · Form

A checkbox represents a single, independent binary setting — on or off — and can additionally represent a mixed state when it summarizes a partially-selected group. Namirial checkboxes are built from --nm-checkbox-* tokens defined in namirial-theme.css, ship in two sizes, and pair with the shared .nm-choice-field label wrapper used by radio and switch.

Overview

A checkbox lets a user opt in or out of one thing at a time. Each checkbox in a list is independent — checking one never unchecks another. Use the mixed state only on a "select all" checkbox that summarizes a group of children where some, but not all, are checked.

When to use

Independent selection:
A single, standalone yes/no setting (Remember me, Send me product updates), or a multi-select list where any number of items — including zero or all — can be checked. Not for mutually exclusive options; use a radio group for that instead.

Select all / mixed state:
A "select all" control that reflects the state of the group beneath it — off, on, or mixed when only some children are checked.

Staged, not instant:
Checkboxes imply the change is queued until the surrounding form is submitted, rather than applying right away. If the setting is always-visible in a settings page and should take effect the instant it's flipped, use a switch instead — it reads more clearly at a glance for that case.

Required state
Show that a checkbox is required before validation, not only after — via a visible marker or short instruction text.

  • Required list (e.g. "select at least one"): show the validation message on the list as a whole, not per item.
  • Required single checkbox (e.g. "I agree to the terms"): show the validation message on that checkbox.
  • If no explicit confirming action is legally or functionally needed, consider a plain info text instead of a checkbox — it saves a click while still making sure the user has seen the requirement.

Anatomy

A checkbox is a single <input type="checkbox"> styled with appearance: none. Everything — box, border, and check glyph — is one element; the glyph is an inline SVG rendered as a mask-image on its ::after pseudo-element, not a separate icon component. When labeled, it's wrapped in a .nm-choice-field <label> alongside the text.

Tokens and values that drive each part

PartRoleSource
ContainerFill, unchecked--nm-checkbox-bg
ContainerFill, checked / mixed--nm-checkbox-checked-bg
ContainerSizewidth / height — 24px default, 20px --sm
ContainerRadiusborder-radius — 8px default, 6px --sm
BorderColor, unchecked--nm-checkbox-border
BorderColor, checked / mixed--nm-checkbox-checked-border
Check glyphColor--nm-checkbox-checked-icon
Check glyphShapeCheckmark when :checked, dash when :indeterminate — swapped mask-image
LabelTypography.nm-choice-field__label — 16px / 400 / Open Sans
Required markColor--nm-choice-field-required
Optional markColor--nm-choice-field-optional
Focus ringColor--palette-focus-700 (light) / --palette-focus-300 (dark), 2px outline, 2px offset

Selection states

A checkbox has three possible selection states. Off and On are set by the user; Mixed can only be set programmatically — there's no way for a person to click a checkbox into "mixed" directly.

off
unselected
on
fully selected
mixed
some children selected

Wiring "select all"

Mixed is a visual and semantic state, not a click target — set it in code from your list's current selection:

const allChecked = items.every((i) => i.checked);
const noneChecked = items.every((i) => !i.checked);
selectAll.indeterminate = !allChecked && !noneChecked;
selectAll.checked = allChecked;
selectAll.setAttribute('aria-checked', selectAll.indeterminate ? 'mixed' : String(allChecked));

Clicking a mixed checkbox should select all children, not toggle each one individually — treat it the same as clicking an unchecked box.

Sizes

Checkbox ships in two sizes — most surfaces should default to 24px and reserve 20px for dense tables and compact filter lists.

default
24×24px · r 8
sm
20×20px · r 6

Interaction states

Every checkbox responds to hover and can be placed in a read-only state that keeps its value visible but locks it against further changes.

Default
Hover
Read-only
Focus

All shown checked

No disabled state

Like buttons, Namirial checkboxes never ship a browser-default "greyed out and dead" disabled look. If a setting truly can't be changed right now, use read-only instead: the value stays legible, the control stays in the layout, and — unlike disabled — its value still submits with the form.

Read-only implementation

The readonly attribute has no native effect on <input type="checkbox">, so the .is-readonly class does the visual work while you pair it with tabIndex={-1} and aria-readonly="true" to make it genuinely non-interactive:

<input type="checkbox" class="nm-checkbox is-readonly" checked readonly tabindex="-1" aria-readonly="true">

pointer-events: none (baked into .is-readonly) blocks clicks; tabindex="-1" removes it from the tab order so Space/Enter can't reach it either.

Focus

Focus is rendered by a 2px outline, 2px offset, using the same focus color as the rest of the system. Never suppress the focus ring — it's the only way keyboard users can tell which checkbox is about to be toggled.

Markup

Pair a checkbox with a label using .nm-choice-field. The label is clickable and toggles the input — you don't need a separate for/id wire-up as long as the input is a descendant of the <label>.

Bare · required · optional

Use .nm-choice-field__required for a setting the user must actively confirm before continuing (rare for checkboxes — most are opt-in by nature). Use .nm-choice-field__optional to reassure the user that leaving it unchecked is fine.

Light & dark theme

Checkbox tokens are remapped under .dark. Contrast and hover directionality are handled for you.

Light data-theme="light"
Dark data-theme="dark"

Both previews are locked — they don't follow the page theme switcher.

Do & don't

✓Do
Use a checkbox for one independent setting the user can flip on its own.
✕Don't
Don't use checkboxes for mutually exclusive options — that's what radio groups are for.
✓Do
Drive the mixed state from real partial selection, and clicking it should select everything.
✕Don't
Don't show a plain checked "select all" when only some items are actually selected — say so with mixed.
✓Do
Use read-only when a value can't change right now but the user still needs to see and submit it.
✕Don't
Don't reach for the native disabled look — it drops the value from the form and gives no reason why.

Accessibility

Semantics

  • Always wrap the input in a <label> (or connect it via for/id) so the hit target includes the text.
  • Group related checkboxes under a <fieldset> with a <legend> when they belong to the same question.

Mixed state

  • Set element.indeterminate = true in JavaScript — there is no HTML attribute for it.
  • Also set aria-checked="mixed" explicitly. Some screen readers don't reliably announce the native indeterminate property on its own.

Read-only

  • Native readonly has no effect on checkboxes. Pair .is-readonly with tabindex="-1" and aria-readonly="true" — this removes it from the tab order (so Space can't reach it) while pointer-events: none blocks clicks, and the field's value still submits with the form.

Keyboard

  • Tab moves focus between checkboxes; Space toggles the focused one.
  • Focus uses :focus-visible, so the ring appears on keyboard focus but not on mouse click.

Labeling

  • Never rely on color alone to communicate checked vs. unchecked — the filled container plus the check glyph both change.
  • If a checkbox has no visible text label (rare), it must have an aria-label.

Playground

<label class="nm-choice-field"> <input type="checkbox" class="nm-checkbox" checked> <span class="nm-choice-field__label">Accept terms and conditions</span> </label>

Token reference

TokenRole
--nm-checkbox-bgContainer fill, unchecked
--nm-checkbox-borderBorder, unchecked
--nm-checkbox-checked-bgContainer fill, checked / mixed
--nm-checkbox-checked-borderBorder, checked / mixed
--nm-checkbox-checked-iconCheck / dash glyph color
--nm-checkbox-hover-bgFill on hover, unchecked
--nm-checkbox-hover-borderBorder on hover, unchecked
--nm-checkbox-checked-hover-bgFill on hover, checked / mixed
--nm-checkbox-checked-hover-iconGlyph color on hover, checked / mixed
--nm-checkbox-readonly-bgFill, read-only + unchecked
--nm-checkbox-checked-readonly-bgFill, read-only + checked / mixed
--nm-checkbox-checked-readonly-iconGlyph color, read-only + checked / mixed
--nm-choice-field-label-fgLabel text color
--nm-choice-field-requiredRequired mark color
--nm-choice-field-optionalOptional mark color

Implementation

No JavaScript component is required for a plain checkbox — it's a native <input> styled by CSS classes. You only need JS to set indeterminate, or to enforce read-only.

<link rel="stylesheet" href="namirial-theme.css">

<label class="nm-choice-field">
  <input type="checkbox" class="nm-checkbox">
  <span class="nm-choice-field__label">
    I agree to the terms of service
    <span class="nm-choice-field__required">(required)</span>
  </span>
</label>

CSS classes

ClassValuesDefault
nm-checkbox--sm20px variant (omit for 24px default)default
:checkedSelection = on—
:indeterminateSelection = mixed (set via JS, not an attribute)—
[readonly] / .is-readonlyRead-only — pair with tabindex="-1"—
nm-choice-field__requiredMarks a field as required—
nm-choice-field__optionalMarks a field as optional—