Checkbox Group

Agentic Experience (AX)

How agents read, build and check a checkbox group, from its accessibility tree to its contract, its DOM and its tests.

Finally, let's look at how agents read, check and verify a checkbox group. They rely on the same accessibility tree as assistive technologies, and on a contract they can check the code against.

Semantic

Agents that drive a browser, through Playwright or a browser MCP server, read the page through its accessibility tree, which is made of roles, names and states. This is how they see the select-all example on this page:

- group "Which emails would you like?":
  - checkbox "All emails" [checked=mixed]
  - checkbox "Product news" [checked]
  - checkbox "Tips and guides"
  - checkbox "Events near you"

A heading or a paragraph above some boxes is only text, so an agent can't tell which boxes belong to which question, any more than a screen reader user can. With a named group, an agent can find the question, pick from it and confirm what it picked:

const emails = page.getByRole("group", { name: "Which emails would you like?" });
await emails.getByRole("checkbox", { name: "All emails" }).check();
for (const box of await emails.getByRole("checkbox").all()) await expect(box).toBeChecked();

Described

Here's the whole contract in one block, for agents to read before they build or review a checkbox group:

component: CheckboxGroup
summary: Lets people pick any number of options from a short list. Its legend names it, and each option is a Checkbox.
standard: https://opencomponents.dev/docs/components/checkbox-group
element: fieldset # its first child is the legend that names it. It takes every attribute that isn't a prop
role: group
props:
  options: { type: "{ value: string, label: string }[]" } # a Checkbox for each, in this order
  name: { type: string } # shared by every box, so the form sends one field with a value per checked option
  value: { type: "string[]", default: [] } # the checked options' values, in their order, when it's bound. Without it, the group keeps its own
  size: { type: [xs, sm, md, lg, xl], default: md }
  orientation: { type: [vertical, horizontal], default: vertical }
  disabled: { type: boolean, default: false } # sets disabled on the fieldset
  selectAll: { type: string } # the label of a box above the options that checks or clears them all
slots:
  default: The legend, which names the group.
events:
  change: Event, from the box that changed, which bubbles up to the fieldset.
states:
  disabled: ":disabled, from <fieldset disabled>, which every box inside it matches too"
  invalid: "[aria-invalid=true], which each option gets too, never :invalid"
  select-all: "> [data-slot=select-all] input:checked when every option is, :indeterminate when only some are"
keyboard:
  Tab: Moves focus to the next box, select-all box included. There's no roving focus.
  Space: Checks or unchecks the focused box. On the select-all box, it checks every option, or clears them all.
parts: [legend, select-all, options] # data-slot, in this order. Only select-all is optional
tokens:
  theme: ["--color--neutral-text"] # and the Checkbox's, through its boxes
  variables: [--checkbox-group--legend--font-size, --checkbox-group--legend--margin-bottom, --checkbox-group--indent, --checkbox-group--row-gap, --checkbox-group--column-gap]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules: https://opencomponents.dev/docs/components/checkbox-group#checklist

It's also published on its own at /raw/docs/components/checkbox-group.yaml, with every rule from the checklist in place of the rules link. Its boxes follow the Checkbox's contract too, so give agents both.

Every rule has a stable ID, like checkbox-group/select-all-state, so you and your agents can cite it in reviews and commits.

Deterministic

The same props always render the same markup, leaving out anything your framework adds for itself, like comments or scoping attributes. For example, here's a group with a select-all box and two options, "Product news" (news) and "Tips and guides" (tips), described by its hint:

<CheckboxGroup name="emails" selectAll="All emails" aria-describedby="emails-hint">
  Which emails would you like?
</CheckboxGroup>

And here's what it renders:

<fieldset class="checkbox-group" aria-describedby="emails-hint" data-size="md" data-orientation="vertical">
  <legend data-slot="legend">Which emails would you like?</legend>
  <div data-slot="select-all">
    <label class="checkbox" data-size="md">
      <input data-slot="box" type="checkbox" />
      <svg data-slot="indicator" viewBox="0 0 16 16" aria-hidden="true">
        <path data-mark="check" d="M3.5 8.5 6.5 11.5 12.5 4.5" />
        <path data-mark="dash" d="M4 8h8" />
      </svg>
      <span data-slot="label">All emails</span>
    </label>
  </div>
  <div data-slot="options">
    <label class="checkbox" data-size="md">
      <input data-slot="box" type="checkbox" name="emails" value="news" />
      <svg data-slot="indicator" viewBox="0 0 16 16" aria-hidden="true">…</svg>
      <span data-slot="label">Product news</span>
    </label>
    <label class="checkbox" data-size="md">
      <input data-slot="box" type="checkbox" name="emails" value="tips" />
      <svg data-slot="indicator" viewBox="0 0 16 16" aria-hidden="true">…</svg>
      <span data-slot="label">Tips and guides</span>
    </label>
  </div>
</fieldset>
  • The size and the orientation are mirrored as data-size and data-orientation, which are always there, and every box takes the group's data-size.
  • Parts are marked with data-slot and always come in the same order: legend, select-all and options. Only the select-all box is optional, and each box inside follows the Checkbox's DOM contract.
  • States change properties and attributes, never elements. The select-all box's mixed state is a property of its <input>, so it never shows up in the markup, and no box is ever replaced as people check and uncheck them, so focus and the screen reader's position survive every change.
  • There's nothing random or generated, like IDs or timestamps, so the markup is the same on the server, on the client and in every snapshot. The legend names the <fieldset> without an ID. The select-all box has no aria-controls, which the APG's example gives it. We leave it out on purpose, since it would need an ID on every option, and the <fieldset> already ties the box to the options inside it.

Verifiable

Every rule in the checklist can be checked, and about two thirds of them automatically.

Unit tests

Every rule that the checklist checks with a unit test has one in CheckboxGroup.test.ts, which you'll find under Reference implementation. We run them on every change to this page, so the code you see always passes them.

In the browser

In the browser, you can run axe to check labels and contrast, and use the keyboard and the select-all box the way an agent would:

import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";

test("the emails group is named, reached one box at a time, and its select-all box checks them all", async ({ page }) => {
  await page.goto("/preferences");

  const emails = page.getByRole("group", { name: "Which emails would you like?" });
  const all = emails.getByRole("checkbox", { name: "All emails" });
  await all.focus();
  await page.keyboard.press("Tab");
  await expect(emails.getByRole("checkbox", { name: "Product news" })).toBeFocused();
  await page.keyboard.press("Space");
  await expect(all).toBeChecked({ indeterminate: true });
  await all.check();
  for (const box of await emails.getByRole("checkbox").all()) await expect(box).toBeChecked();

  const { violations } = await new AxeBuilder({ page })
    .withTags(["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"])
    .analyze();
  expect(violations).toEqual([]);
});

Some things still need a human, so try your groups with the keyboard alone, with a screen reader, which should announce the legend as you move into the group and the select-all box as mixed, and at 200% zoom, with forced colours and more contrast turned on. You can emulate the last two in Chrome DevTools, under Rendering.

During development

Our reference implementation also warns you in the console as soon as a group renders without a legend, which leaves screen reader users with a list of boxes and no question.

Prompts

You can point your agent straight at this page. To build a checkbox group, you could use:

Build a Checkbox Group component for our React design system that meets the Open Components
Checkbox Group standard: https://opencomponents.dev/raw/docs/components/checkbox-group.md

Follow its API, its DOM contract and its tokens. Port its tests and make them pass.
Then check your work against every rule in its checklist with a Component or Both
scope, and cite the rule ID for any rule you can't meet.

To review one, the contract is enough:

Review our Checkbox Group against the Open Components Checkbox Group contract:
https://opencomponents.dev/raw/docs/components/checkbox-group.yaml

For each rule with a component or both scope, report pass or fail with the rule ID
and the evidence from our code. Then fix the failures.

And to review how a screen uses its checkbox groups:

Review how our preferences page uses its checkbox groups against the Open Components
Checkbox Group contract: https://opencomponents.dev/raw/docs/components/checkbox-group.yaml

For each rule with a usage or both scope, report pass or fail with the rule ID and
the evidence from our code. Then fix the failures.

AX Checklist

RuleLevelScopeRequirementBasisCheck
checkbox-group/role-and-nameMustComponentIt can be found by its legend alone, with getByRole("group", { name }), and each option inside it with getByRole("checkbox", { name })WCAG 4.1.2 (A)Unit test
checkbox-group/deterministic-domMustComponentThe same props render the same markup, with no generated IDs, and states change properties and attributes, never elementsOpen ComponentsUnit test
checkbox-group/data-attributesShouldComponentThe size and the orientation are mirrored as data-size and data-orientation, and parts are marked with data-slotOpen ComponentsUnit test
checkbox-group/dev-warningsShouldComponentIt warns during development when a group has no legendOpen ComponentsUnit test
Copyright © 2026