Checkbox Group

User Interface (UI)

How a checkbox group looks, from its parts, sizes and orientation to its states and tokens.

Let's start with how a checkbox group looks: its parts, its sizes, its orientation and how it shows each of its states.

Anatomy

  1. Group — The <fieldset>, which holds the legend and every checkbox, and carries the size and orientation.
  2. Legend — The <legend>, its first child, which names the group, usually with a question.
  3. Select all — Optional. A Checkbox that checks or clears every option, and shows whether all, some or none are checked.
  4. Options — A Checkbox for each option, which all share one name.
  5. Hint and error — Optional. Paragraphs of their own, after the group, which its <fieldset> points to by their id.

Every group has a legend and its options. The select-all box is only there when you ask for one, and its options then sit in from it, so they read as its children. The hint and the error message sit outside the group, in the page, and its <fieldset> points to them by their id. The parts always render in the same order, which you can see in the DOM contract.

Sizes

The size prop sets the size of every checkbox in the group, and of its legend, and md is the default. The names are the same as the Checkbox's, so a group reads at the same size as the inputs and buttons next to it.

Which emails would you like? (xs)
Which emails would you like? (sm)
Which emails would you like? (md)
Which emails would you like? (lg)
Which emails would you like? (xl)
<CheckboxGroup size="xs" name="emails" options={emails}>Which emails would you like?</CheckboxGroup>
{/* …sm, md, lg, xl */}
SizeLegendOptions' textIndent under select all
xs12px12px20px
sm14px14px24px
md14px14px26px
lg16px16px28px
xl16px16px34px

Sizes are set in rem, and the table shows the values we recommend at the default text size of 16px. The legend is the same size as its options' text, in a heavier weight, so it reads as their heading without shouting over the rest of the form. The indent is the select-all box's width plus its gap, so each option's box lines up with the select-all box's label. Each checkbox keeps its 24 × 24px hit area at every size (see the Checkbox's Sizes).

Orientation

Options run in a column by default, one per line, and orientation="horizontal" puts them in a row, which wraps onto the next line when it runs out of room.

Which emails would you like?
How can we reach you?
<CheckboxGroup name="emails" options={emails}>Which emails would you like?</CheckboxGroup>
<CheckboxGroup name="contact" options={contact} orientation="horizontal">How can we reach you?</CheckboxGroup>

We recommend the column for almost every group, which is why it's the default. Every label starts at the same edge, so people can scan down the list without hunting for where the next option begins, and every box sits right next to its own label, so nobody wonders whether a box belongs to the label on its left or on its right. A long label wraps under itself rather than pushing the next option onto another line, and the column still works on a narrow phone and at 200% zoom (WCAG 1.4.10). Keep the row for two or three options with short labels, like "Email", "Phone" and "Post".

States

Each checkbox shows its own states, as on the Checkbox page. On top of those, the group can be disabled or invalid as a whole, and a select-all box shows whether all, some or none of its options are checked.

Which emails would you like?
Some options checked (try tabbing through them)
Which emails would you like?
With a select-all box, mixed
Which emails would you like?
Disabled
Which emails would you like?

Pick at least one email to sign up for

Invalid
<CheckboxGroup name="emails" options={emails} defaultValue={["news"]}>Which emails would you like?</CheckboxGroup>

<CheckboxGroup name="emails" options={emails} defaultValue={["news"]} selectAll="All emails">
  Which emails would you like?
</CheckboxGroup>

<CheckboxGroup name="emails" options={emails} defaultValue={["news"]} disabled>Which emails would you like?</CheckboxGroup>

<CheckboxGroup name="emails" options={emails} aria-invalid="true" aria-describedby="emails-error">
  Which emails would you like?
</CheckboxGroup>
<p id="emails-error">Pick at least one email to sign up for</p>
StateSelectorRecommended look
Disabled:disabled, which a <fieldset disabled> matchesThe legend at 50% opacity, and every checkbox looks disabled
Invalid[aria-invalid="true"], which each option gets tooEach option's box with the Checkbox's invalid border
Select allIts <input>'s :checked and :indeterminateThe Checkbox's check, or its dash while only some options are checked

The group draws no state of its own on the boxes. A disabled <fieldset> disables every checkbox inside it, and each one looks disabled from :disabled, as it would on its own. An invalid group passes aria-invalid on to its options, so each box shows the Checkbox's invalid border, and the error message says what's wrong in words, since a red border alone can't (WCAG 1.4.1). None of the states change the group's size or position. You'll find how each state behaves under Every state.

Tokens

The group itself reads one theme token, and each of its checkboxes reads the Checkbox's own five:

TokenUsed for
--color--neutral-textThe legend, which needs 4.5:1 against the page, like any text

The legend is drawn in the same colour as the checkboxes' labels, so it has the same contrast as they do: 4.5:1 or more against the page with our reference tokens (WCAG 1.4.3). You'll find the tokens themselves in the Button's tokens.css, which you can copy into your theme.

If you need to tune a group, you can set its own variables, such as --checkbox-group--row-gap or --checkbox-group--legend--font-size, from a class. Each box sets the Checkbox's variables on itself, so a value on the group doesn't reach them. Set those from the same class with a descendant selector, like .brand .checkbox, written so that your framework's style scoping lets it through to the boxes, which the group renders: :global() in CSS Modules, Svelte and Astro, :deep() in Vue and ::ng-deep in Angular, as below. The Checkbox Group's styles sit in the components cascade layer, so your class wins without any specificity tricks:

Emails.tsx
import styles from "./Emails.module.css";

export function Emails() {
  return (
    <CheckboxGroup name="emails" options={emails} className={styles.brand}>
      Which emails would you like?
    </CheckboxGroup>
  );
}
Emails.module.css
.brand {
  --checkbox-group--row-gap: 0.75rem;
}
/* The boxes' own class isn't this module's, so it stays global. */
.brand :global(.checkbox) {
  --checkbox--box--radius: 0;
}

UI Checklist

RuleLevelScopeRequirementBasisCheck
checkbox-group/legend-contrastMustComponentThe legend has a contrast of 4.5:1 against the pageWCAG 1.4.3 (AA)axe color-contrast, contrast checker
checkbox-group/vertical-by-defaultShouldComponentOptions run in a column unless it's set to a row, and a row wraps rather than overflowingOpen ComponentsVisual regression test
checkbox-group/option-spacingShouldComponentOptions sit at least 8px apart in a column and 24px apart in a rowOpen ComponentsVisual regression test
checkbox-group/select-all-indentShouldComponentOptions under a select-all box sit in from it by its box and gap, so their boxes line up with its labelOpen ComponentsVisual regression test
checkbox-group/resizableMustComponentThe legend, the gaps and the indent are sized in rem, and the legend and a row of options wrap instead of being clipped or scrolling sidewaysOpen Components, beyond WCAG 1.4.4, 1.4.10, 1.4.12 (AA)200% zoom
checkbox-group/disabled-lookMustComponentA disabled group shows its legend as disabled along with its boxes, in GrayText in forced colours modeOpen ComponentsVisual regression test, emulation
checkbox-group/stable-sizeMustComponentNo state changes the group's size or positionOpen ComponentsVisual regression test
Copyright © 2026