Components / forms
CheckboxGroup
The select-all is part of the group rather than a separate component, because its indeterminate state is a fact about the group and nothing else can compute it.
Summary
Several checkboxes that answer one question — privacy settings, which sections to publish.
| APG pattern | Checkbox |
| Built on | native elements |
| Status | stable |
| Since | 0.1.0 |
<CheckboxGroup id="privacy" legend="Privacy" description="These apply to the published site only."
defaultValue={['search']} options={…} onChange={setPrivacy} />
Anatomy
A <fieldset data-se="checkbox-group"> with a <legend>, holding native checkboxes. An optional select-all sits above them.
| Part | Element | Required |
|---|---|---|
controller | <div> | no |
description | <p> | no |
error | <p> | no |
legend | <legend> | yes |
options | <div> | yes |
Properties
| Property | Values | Default | What it does |
|---|---|---|---|
id | string | required | Required — used to build the legend, option, description and error ids. |
legend | React.ReactNode | required | The question the checkboxes answer. Announced with every option. |
description | React.ReactNode | none | A line under the legend, before the options. Announced with the group. |
options | CheckboxOption[] | required | The checkboxes, in display order. Each is an independent answer, which is why each is its own tab stop. |
value | string[] | none | The checked values. Passing this makes the group controlled; omitting it lets the group keep its own state. |
defaultValue | string[] | none | Which options start checked when the group keeps its own state. |
onChange | (value: string[]) => void | none | Called with the full set of checked values, not the one that changed. |
controller | React.ReactNode | none | Label for APG's tri-state parent checkbox. It reports mixed when some but not all children are checked, selects all from mixed or empty, and clears all when already full. Omit it for a plain group. |
kind | checkbox · switch | checkbox | switch renders the options as switches; the grouping is identical. |
orientation | vertical · horizontal | vertical | Stacks the options or lays them in a row. A row is only honest when every label is short. |
error | React.ReactNode | none | Shown and wired to aria-describedby. Never rely on color alone. |
States
_None. Rest is the only condition it has._
The select-all is indeterminate when some but not all are checked, set through the DOM property because HTML has no such attribute.
Behavior
Each box is independent. A select-all checks or clears the set.
Accessibility
Use this instead of a bare stack of Choice controls. A screen-reader user meeting three loose checkboxes hears three unrelated toggles with no idea what they have in common; the legend is announced with each option and supplies exactly that.
The tri-state select-all announces as mixed, not as a third checked value the reader has to interpret.
Keyboard
| Key | Result |
|---|---|
Tab | Moves to each checkbox in turn — they are independent, so each is its own stop |
Space | Toggles one |
Roles, states and properties
| Attribute | Set by |
|---|---|
aria-checked | set at render |
aria-describedby | set at render |
aria-labelledby | set at render |
role | always group |
Focus
Not one tab stop. Checkboxes are independent answers, so each takes its own — skipping one because a sibling is checked would hide it. That is what separates this from RadioGroup, where the group is one stop because only one answer can stand.
Screen reader
Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.
Contrast and target size
npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.
It sets no target size of its own; anything interactive inside it brings its own.
Tested
test/apg/batch-d.test.jsxtest/apg/checkbox-group.test.jsxtest/browser/upstream-natives.spec.mjs
Known gaps
- 1 upstream ref — their checkbox controls a group of other checkboxes; ours is CheckboxGroup, which owns the relationship
Appearance
| Token | Used for |
|---|---|
--se-border | border-block-end |
--se-border-width | border-block-end |
--se-leading-body | line-height |
--se-leading-ui | line-height |
--se-negative | color |
--se-space-1 | gap, margin-block-start |
--se-space-2 | margin-block-end, margin-block-start, padding-block-end |
--se-space-4 | gap |
--se-text | color |
--se-text-muted | color |
--se-text-sm | font-size |
--se-weight-medium | font-weight |
Motion
None beyond the control's own.
Content
The legend names the set as a category rather than a question, because the answers are not exclusive — Privacy, not What should be private?
Usage
When not to use it. For a one-of-many choice — that is RadioGroup. For one setting on its own — that is Choice, and wrapping a single checkbox in a fieldset makes a reader hear the question twice.
It renders its own options, and that is the point. The options array becomes Choice controls this component owns — the fieldset, the legend and the wiring between them are the reason it exists, and they are the parts most often got wrong by hand. Reach for Choice directly only for a lone control that answers no group's question.
When not to use it. For one of many, where the platform gives arrow keys and the one-of-many constraint for free — that is RadioGroup.
Notes
The select-all is part of the group rather than a separate component, because its indeterminate state is a fact about the group and nothing else can compute it.