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.

Rendered from the package · open on its own

Summary

Several checkboxes that answer one question — privacy settings, which sections to publish.

APG patternCheckbox
Built onnative elements
Statusstable
Since0.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.

PartElementRequired
controller<div>no
description<p>no
error<p>no
legend<legend>yes
options<div>yes

Properties

PropertyValuesDefaultWhat it does
idstringrequiredRequired — used to build the legend, option, description and error ids.
legendReact.ReactNoderequiredThe question the checkboxes answer. Announced with every option.
descriptionReact.ReactNodenoneA line under the legend, before the options. Announced with the group.
optionsCheckboxOption[]requiredThe checkboxes, in display order. Each is an independent answer, which is why each is its own tab stop.
valuestring[]noneThe checked values. Passing this makes the group controlled; omitting it lets the group keep its own state.
defaultValuestring[]noneWhich options start checked when the group keeps its own state.
onChange(value: string[]) => voidnoneCalled with the full set of checked values, not the one that changed.
controllerReact.ReactNodenoneLabel 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.
kindcheckbox · switchcheckboxswitch renders the options as switches; the grouping is identical.
orientationvertical · horizontalverticalStacks the options or lays them in a row. A row is only honest when every label is short.
errorReact.ReactNodenoneShown 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

KeyResult
TabMoves to each checkbox in turn — they are independent, so each is its own stop
SpaceToggles one

Roles, states and properties

AttributeSet by
aria-checkedset at render
aria-describedbyset at render
aria-labelledbyset at render
rolealways 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.jsx
  • test/apg/checkbox-group.test.jsx
  • test/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

TokenUsed for
--se-borderborder-block-end
--se-border-widthborder-block-end
--se-leading-bodyline-height
--se-leading-uiline-height
--se-negativecolor
--se-space-1gap, margin-block-start
--se-space-2margin-block-end, margin-block-start, padding-block-end
--se-space-4gap
--se-textcolor
--se-text-mutedcolor
--se-text-smfont-size
--se-weight-mediumfont-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.