Checkbox Group

User Experience (UX)

How a checkbox group behaves for every person, input and setting, so it's accessible, predictable, clear in every state and adaptive.

Now let's look at how a checkbox group behaves for every person, input and setting. A good checkbox group is accessible, predictable, designed in every state and adaptive.

Accessible

Use a fieldset and a legend

A <fieldset> gives you the group role and disabled for every control inside it, and a <legend> as its first child gives the group its name, all for free. Screen readers announce the legend as people move into the group, so they hear "Which emails would you like?" before "Product news, checkbox, not checked", rather than a list of boxes with no question. A <div> with a paragraph above it only looks like a group, and a <div role="group"> still needs aria-labelledby to be named, and can't disable anything.

{/* Do: the legend names the group, and screen readers announce it with each box */}
<fieldset>
  <legend>Which emails would you like?</legend>
  <label><input type="checkbox" name="emails" value="news" /> Product news</label>
  <label><input type="checkbox" name="emails" value="tips" /> Tips and guides</label>
</fieldset>

{/* Don't: the question is just a paragraph, so the boxes are announced without it */}
<div>
  <p>Which emails would you like?</p>
  <label><input type="checkbox" name="news" /> Product news</label>
  <label><input type="checkbox" name="tips" /> Tips and guides</label>
</div>

The legend has to be the <fieldset>'s first child, which the HTML Standard asks for, and which is what names the group. The Checkbox Group renders its legend there for you, and resets the fieldset's border and padding, so it lines up with the rest of your form, without the browser's frame around it.

Name and describe the group

The legend says what the options are for, and it's best written as the question they answer, like "Which emails would you like?". It's the group's accessible name, so screen readers read it as people move into the group, voice control users can say it, and tests and agents use it to find the group.

Which emails would you like?

Pick as many as you like. You can stop them at any time.

<CheckboxGroup name="emails" options={emails} aria-describedby="emails-hint">Which emails would you like?</CheckboxGroup>
<p id="emails-hint">Pick as many as you like. You can stop them at any time.</p>
  • Every group has a visible legend (WCAG 1.3.1, 3.3.2). Keep it short, since screen readers can repeat it for each box, and put anything longer in the hint.
  • A hint, like "Pick as many as you like", goes in the group's description. Keep it out of the legend, so it doesn't become part of the name, and point aria-describedby on the group at its id, which is what the APG asks for a group's descriptive text.
  • An error message goes in the description too, after the hint, so the group's aria-describedby lists both IDs, the hint's first (see Pick at least one).
  • Each option is a Checkbox, so its label follows the Checkbox's rules, from checkbox/visible-label to checkbox/positive-label. The options take a value and a label and nothing else, so a hint about one option goes in the group's hint, naming the option it's about.

Contrast and size

  • The legend needs a contrast of at least 4.5:1 with its background (WCAG 1.4.3). A disabled group is exempt, but it should still be readable.
  • Each box, its focus ring and its hit area follow the Checkbox's Contrast and size.
  • Stacked options sit 8px apart, so a tap that lands between two of them doesn't toggle the wrong one, and every option keeps its own 24 × 24px hit area (WCAG 2.5.8).

Predictable

Keyboard

KeyCheckbox Group
Tab, Shift+TabMoves focus to the next or previous checkbox, select-all box included
SpaceChecks or unchecks the focused checkbox
Arrow keysDon't move focus between checkboxes

Every checkbox in a group is a stop of its own in the tab order, which is what the APG's mixed-state example does, and what native checkboxes do. That's different from a Radio Group, where Tab reaches the group once and the arrow keys move between its options, because only one of them can be checked. In a checkbox group, people check each box on its own, so each one gets its own stop, and you don't need any roving focus to build one.

Select all

A select-all box lets people check or clear every option at once, which saves a lot of clicks in a long list. It's a Checkbox above the options that shows what they add up to: checked when every option is, unchecked when none is, and mixed, with a dash, when only some are. Screen readers announce it as "mixed", or "partially checked", so people know what it stands for before they press it.

Which emails would you like?

You'll get 1 of 3

const [picked, setPicked] = useState(["news"]);

<CheckboxGroup name="emails" options={emails} value={picked} onValueChange={setPicked} selectAll="All emails">
  Which emails would you like?
</CheckboxGroup>
<p>You'll get {picked.length} of {emails.length}</p>
  • Checking it, from unchecked or from mixed, checks every option, and unchecking it clears them all, which is what the APG asks for. Try it above: check one option, and the box turns mixed, then press it, and every option is checked.
  • It doesn't go back to the options people had picked before, the way the APG's own example does as a third step. The APG leaves that up to you, and we leave it out, since a box that does something different on every third press is hard to predict, and it's never clear which earlier selection it'll bring back.
  • Its state is worked out from the options every time they change, never kept on its own, so it can't disagree with them, as checkbox/derived-mixed asks.
  • It has no name, so its form never sends it. It stands for the options, and they're what the form sends.
  • Label it with what it selects, like "All emails", rather than "Select all" on its own, so it still makes sense out of context. It's best kept for longer lists, or for lists that people often want all of, since in a group of two or three it's one more box to read.

Keep options in a column

People read a checkbox group from top to bottom, one option per line, and that's how the Checkbox Group lays them out unless you ask for a row (see Orientation). A column keeps each box next to its own label, lets long labels wrap under themselves, and still fits on a phone, which a row of more than a few options doesn't.

Order the options in a way people can predict, like from most to least common, or alphabetically for a long list, and keep the same order every time the group shows up, so people who've used it before find each option where they left it.

Every state

Disabled

disabled on the Checkbox Group sets disabled on its <fieldset>, which disables every checkbox inside it, select-all box included. None of them can be focused, toggled or sent with the form, and each one looks disabled on its own, since the Checkbox's look comes from :disabled, which matches inside a <fieldset disabled> too (see checkbox/disabled-inert). The legend fades with them, so the whole group reads as one that's out of use.

As with a single checkbox, we recommend disabling as little as you can, since keyboard users can't reach a disabled group to find out why it's disabled (checkbox/explain-disabled). Explain why next to it, or leave the group out until it applies.

Pick at least one

Most checkbox groups are optional, since checking nothing is an answer too. Sometimes people have to pick at least one, though, like choosing which emails to sign up for on a page that's only there to sign up. That's a rule about the group, and HTML has no way to say it. required on a checkbox means that box has to be checked, so putting it on every box asks people to check them all, and putting it on one box asks for that one in particular. So say it in the hint, check it in your own code when people submit, and report it on the group.

Which emails would you like?

Pick at least one.

import { useRef, useState, type FormEvent } from "react";
import { flushSync } from "react-dom";

export function SignUp() {
  const [picked, setPicked] = useState<string[]>([]);
  const [error, setError] = useState("");
  const [status, setStatus] = useState("");
  const group = useRef<HTMLFieldSetElement>(null);

  function update(value: string[]) {
    setPicked(value);
    // Picking one fixes the error, so it clears right away.
    if (value.length) setError("");
  }

  async function signUp(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!picked.length) {
      // Focus the first box once the error is in the page, so screen readers read it with the group.
      flushSync(() => {
        setError("Pick at least one email to sign up for");
        setStatus("Couldn't sign you up. Pick at least one email.");
      });
      group.current?.querySelector("input")?.focus();
      return;
    }
    await api.signUp(picked);
    setStatus("You're signed up");
  }

  return (
    <form noValidate onSubmit={signUp}>
      <CheckboxGroup
        ref={group}
        name="emails"
        options={emails}
        value={picked}
        onValueChange={update}
        aria-invalid={error ? true : undefined}
        aria-describedby={error ? "emails-hint emails-error" : "emails-hint"}
      >
        Which emails would you like?
      </CheckboxGroup>
      <p id="emails-hint">Pick at least one.</p>
      {error && <p id="emails-error">{error}</p>}
      <Button type="submit" color="primary">Sign up</Button>
      <p role="status">{status}</p>
    </form>
  );
}
  • Say that people need to pick one before they try, in the hint or in the legend, like "Pick at least one" (WCAG 3.3.2). There's no required to announce it for you.
  • Check it when people submit, rather than as they move through the boxes, since every box is unchecked until they've picked one. Once the error shows, clear it as soon as they check a box.
  • Set aria-invalid="true" on the group while its error message shows, and add the message to the group's aria-describedby, after the hint (WCAG 3.3.1). The Checkbox Group passes aria-invalid on to each option, so every box shows the invalid border and is announced as invalid as people reach it, since not every screen reader reads a group's description when focus moves into it.
  • Say what to pick, as in "Pick at least one email to sign up for", rather than "This field is required" (WCAG 3.3.3).
  • When a submit fails, move focus to the group's first checkbox, once the error is in the page, as on the Checkbox. The status message says what happened even when the focus doesn't move.
  • Put novalidate on the form, as on the Checkbox and the Input, so the browser's own error bubbles don't compete with yours. The same goes for a group with a limit, like at most three options: check it in your code and report it on the group.

Adaptive

Pointer, touch and pen

  • Each option toggles from anywhere in its box's hit area or on its label, as described in the Checkbox's Pointer, touch and pen. A press on the legend does nothing, since it names the group rather than one of its boxes.
  • Stacked options sit 8px apart, and options in a row 24px apart, so a tap that lands between two doesn't toggle the wrong one.

More contrast

When someone asks for more contrast (prefers-contrast: more), each box's border is drawn at full strength, as on the Checkbox. The legend already passes, so it stays as it is.

Forced colours

Windows contrast themes replace every colour with the user's own. Each box keeps its own forced-colours look, as on the Checkbox, and a disabled group's legend is drawn in GrayText, the system's colour for disabled text, like its boxes.

Colour scheme

The tokens use light-dark(), which follows color-scheme, so the Checkbox Group adapts to your light and dark themes on its own.

Text size and spacing

The legend, the gaps and the indent are set in rem, so the whole group grows with its text at 200% text size (WCAG 1.4.4) and with wider line, letter and word spacing (WCAG 1.4.12). A long legend wraps rather than being clipped, and a row of options wraps onto the next line rather than scrolling sideways at 320px wide (WCAG 1.4.10).

Languages and direction

The Checkbox Group indents its options with padding-inline-start, and each Checkbox lays out its box and label in a grid that follows the reading direction, so the whole group mirrors in right-to-left languages, with no extra work on your part:

ما الرسائل التي تريدها؟
<div dir="rtl" lang="ar">
  <CheckboxGroup name="emails" options={emails} defaultValue={["news"]} selectAll="كل الرسائل">
    ما الرسائل التي تريدها؟
  </CheckboxGroup>
</div>
  • The options sit in from the select-all box on the right in a right-to-left layout, since the indent is on the start side.
  • Remember to translate the legend, the options, the hint and the error message along with the rest of the page, and to mark any that are in another language with lang, so that screen readers pronounce them correctly (WCAG 3.1.2).

UX Checklist

RuleLevelScopeRequirementBasisCheck
checkbox-group/fieldsetMustComponentIt's a <fieldset> whose first child is the <legend> that names it, so it has the group role and the legend as its nameWCAG 1.3.1, 4.1.2 (A), HTMLUnit test
checkbox-group/legend-questionMustUsageEvery group has a visible legend that says what its options are for, ideally as the question they answerWCAG 1.3.1, 3.3.2 (A)Review
checkbox-group/describedShouldBothA hint and an error message sit outside the legend and are linked to the <fieldset> with aria-describedby, the hint's ID firstWCAG 1.3.1 (A), APGUnit test, screen reader
checkbox-group/keyboardMustComponentTab reaches each box in turn, select-all box included, Space toggles the focused one, and the arrow keys never move focus between themWCAG 2.1.1 (A), APGUnit test
checkbox-group/select-all-stateMustComponentThe select-all box is checked when every option is, unchecked when none is, and mixed when only some are, worked out from them whenever they changeWCAG 4.1.2 (A), APGUnit test
checkbox-group/select-all-actionMustComponentChecking the select-all box, from unchecked or mixed, checks every option, and unchecking it clears them all, without going back to an earlier selectionAPG, Open ComponentsUnit test
checkbox-group/select-all-labelShouldUsageA select-all box says what it selects, like "All emails", and only comes with lists that are long or often picked in fullOpen ComponentsReview
checkbox-group/verticalShouldUsageOptions run in a column, and only two or three short ones run in a rowOpen ComponentsReview
checkbox-group/predictable-orderShouldUsageOptions come in an order people can predict, like most to least common or alphabetical, and keep it every timeOpen ComponentsReview
checkbox-group/at-least-oneMustUsageA group people have to pick from says so in its legend or hint, and is checked in code when the form is sent, never with required on its boxesHTML, WCAG 3.3.2 (A)Review
checkbox-group/error-messageMustUsageAn invalid group has an error message, in text and linked to the <fieldset> with aria-describedby, that says what to pickWCAG 1.4.1, 3.3.1 (A), 3.3.3 (AA)Screen reader
checkbox-group/invalid-stateMustBothThe group sets aria-invalid="true" while its error message shows, and each of its options looks and reads invalid with itWCAG 4.1.2 (A), Open ComponentsUnit test
checkbox-group/focus-on-errorShouldUsageA failed submit moves focus to the group's first box, once the error is in the pageOpen Components, beyond WCAG 3.3.1 (A)Keyboard
checkbox-group/disabled-inertMustComponentA disabled group sets disabled on its <fieldset>, so no box in it, select-all box included, can be focused, toggled or sentHTMLUnit test
checkbox-group/mirrors-in-rtlShouldComponentThe indent and a row of options follow dir, with logical propertiesOpen ComponentsReview

FAQ

Checkbox Group or something else?

A Checkbox Group lets people pick any number of options from a short list. For anything else, another component will do a better job:

When people need to…Use
Pick any number of options out of a fewCheckbox Group
Say yes or no to one thing, like agreeing to the termsCheckbox
Pick one option out of a fewRadio Group
Pick one option, or several, out of manySelect, or Combobox
Flip settings that take effect right away, like alertsA Switch for each
Select rows in a tableA Checkbox in each row, with an aria-label

The Radio Group, the Select and the Combobox are on our Roadmap.

Copyright © 2026