Component contract
Every checkbox in the admin is <UiCheckbox>. The linter scans the React source and flags any <input type="checkbox"> not rendered through it.
checked: boolean | "indeterminate"— controlled.onChange: (next) => void— fires with the new value (boolean; never "indeterminate" — that's a display-only state).label: ReactNode— required, always rendered. Clicking the label toggles the box.helperText?: string— appears below the row.disabled?: booleanrequired?: boolean— adds visible asterisk +aria-required.error?: string— when set, renders inline below the row.
Interaction surface
-
Render: box left, label right, both clickable.
The label is associated via
<label>wrapper orhtmlFor. Clicking anywhere in the row (box or label text) toggles. Helper text and error are NOT clickable for toggle — they're informational. -
Click: toggles state.
From unchecked → checked (or indeterminate → checked). From checked → unchecked. Indeterminate is set externally only; the user cannot click TO indeterminate.
-
Keyboard: Space toggles; Enter does not.
Space is the standard checkbox toggle. Enter typically submits a parent form; if the checkbox steals Enter, that's a regression. Tab moves focus through.
-
Focus ring is always visible on keyboard focus.
2px ring with contrast ≥ 3:1 against the field background. Never removed via
outline:nonewithout a replacement. -
Indeterminate state.
Visually rendered as a horizontal bar (—) inside the box, distinct from the checkmark.
aria-checked="mixed". Used for "select all" controls when some-but-not-all children are selected.
Failure modes
Label click does not toggle
Trigger: rendered label uses a non-associated <span>; clicking it does nothing.
Click handlers must be on the <label> element with proper htmlFor, OR the label must wrap the input. The harness asserts: click on label text, observe checked state changed.
Focus ring removed via outline:none
Trigger: a designer wanted "cleaner" checkboxes and removed the focus outline.
Focus ring is mandatory; replacement (box-shadow or pseudo-element) is required if outline is suppressed. Linter scans component CSS for outline:none rules without a sibling focus-state replacement.
Enter inside checkbox submits something unexpected
Trigger: focus is on a checkbox inside a form, user presses Enter intending to submit.
Default browser behavior: Enter on a focused checkbox submits the form (assuming there's an implicit submit button). Component preserves this default — it does NOT consume Enter. The harness asserts: Enter on focused checkbox triggers form submit.
Indeterminate not visually distinct from unchecked
Trigger: indeterminate state renders as the unchecked box with no marker.
Indeterminate must show a visible marker (typically a horizontal dash). The harness asserts: render indeterminate, screenshot the box, assert visual delta from unchecked.
Disabled checkbox is still clickable
Trigger: disabled=true is rendered but clicks still toggle the state.
Both the underlying <input> has disabled AND the wrapping label has aria-disabled="true" AND pointer-events: none on the entire row (so label click is also blocked). Harness clicks the disabled checkbox and asserts state did NOT change.
Empty count breaks the label
Trigger: a copy-option label like "Copy & access types" expects to be appended with " (3 access types)" but the count is 0.
Convention: callers should suppress the count parenthetical when count is 0 OR show it as "(none)". Component does NOT make this decision; it renders the label as given. But the linter checks for templates like "$\{label\} (${count} ${noun})" and recommends a guarded form. Soft check, not enforced.
Accessibility
- Underlying element is
<input type="checkbox">withidand a wrapping orhtmlFor-linked<label>. aria-checkedreflects state including"mixed"for indeterminate.aria-required="true"when required.aria-invalid="true"+aria-describedbywhen error is set.- Color contrast: label text ≥ 4.5:1, focus ring ≥ 3:1, checkmark ≥ 3:1 against the box's checked background.
- axe-clean at severity ≥ serious.
Stable test attributes
Visibility teeth. Each attribute must be present AND effectively visible when the checkbox is rendered. Hiding without removal is a Ratchet violation.
| data-test | Where | Purpose |
|---|---|---|
ui-checkbox | Wrapping <label> | Component identity marker; covers entire clickable row |
ui-checkbox-input | The underlying <input type="checkbox"> | The actual input element |
ui-checkbox-label | Inside ui-checkbox | The label text content |
ui-checkbox-helper | Below the row | Helper text region |
ui-checkbox-error | Below the row | Error region; visible only in error state |
Agent test plan
Standalone probes against /admin-test/ui-checkbox-fixture with variants: unchecked, checked, indeterminate, disabled, required, with error.
Probe list
- click-box-toggles: click ui-checkbox-input, assert checked changed
- click-label-toggles: click ui-checkbox-label, assert checked changed (validates label association)
- space-toggles: focus, press Space, assert checked changed
- enter-does-not-toggle: focus, press Enter, assert checked unchanged AND parent form submitted
- focus-ring-visible: focus the checkbox, screenshot, assert visual delta from unfocused
- indeterminate-visual-distinct: render indeterminate, assert distinct from unchecked
- aria-checked-mixed: render indeterminate, assert input has aria-checked="mixed"
- disabled-no-toggle: render disabled, click box AND click label, assert checked unchanged
- error-aria-invalid: render with error, assert aria-invalid="true" AND aria-describedby points to ui-checkbox-error
- required-asterisk-and-aria: render required, assert label contains visible asterisk AND aria-required="true"
- color-contrast-label: assert label text ≥ 4.5:1
- color-contrast-focus-ring: assert focus ring ≥ 3:1