Checkbox

Agentic Experience (AX)

How agents read, build and check a checkbox, 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. 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 states example on this page:

- checkbox "Email me updates"
- checkbox "Email me updates" [checked]
- checkbox "Select all" [checked=mixed]
- checkbox "Text me updates" [disabled]
- checkbox "Email me updates" [checked] [disabled]
- checkbox "Text me updates"
- checkbox "Email me updates" [checked]
- checkbox "I agree to the terms" [invalid]

A <div> that only looks like a checkbox has no role and no state, so an agent can't tell whether it's checked, any more than a screen reader user can. With a role, a name and a state, an agent can find the box, check it and confirm that it took:

const terms = page.getByRole("checkbox", { name: "I agree to the terms" });
await terms.check();
await expect(terms).toBeChecked();

Described

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

component: Checkbox
summary: Says yes or no to one thing, like agreeing to the terms. The label it wraps names it.
standard: https://opencomponents.dev/docs/components/checkbox
element: input # type="checkbox", inside the label that names it, which takes class and style
role: checkbox
props:
  size: { type: [xs, sm, md, lg, xl], default: md }
  checked: { type: boolean, default: false } # when it's bound. Without it, the box keeps its own
  indeterminate: { type: boolean, default: false } # set as a property. The first click clears it, and a bound value with it
  disabled: { type: boolean, default: false }
  readonly: { type: boolean, default: false } # sets aria-readonly, since HTML ignores readonly on a checkbox, and cancels the click
slots:
  default: The label, which names the checkbox. A press on it toggles the box, unless it's read-only.
events:
  change: Event, from the <input>. Fires when people check or uncheck it.
states:
  hover: ":hover, in @media (hover: hover)"
  focus: "> input:focus-visible"
  checked: "> input:checked"
  indeterminate: "> input:indeterminate"
  disabled: ":has(> input:disabled), which a <fieldset disabled> sets too"
  read-only: "> input[aria-readonly=true], never :read-only"
  invalid: "> input[aria-invalid=true], never :invalid"
keyboard:
  Tab: Moves focus to the box.
  Space: Checks or unchecks it, and clears the indeterminate state, unless it's read-only.
  Enter: Never toggles it.
parts: [box, indicator, label] # data-slot, in this order. The box is the <input>
tokens:
  theme: ["--color--neutral-text", "--color--primary", "--color--primary-contrast", "--color--error-text", "--color--focus"]
  variables: [--checkbox--box--size, --checkbox--box--radius, --checkbox--box--border-color, --checkbox--box--fill, --checkbox--indicator--color, --checkbox--gap, --checkbox--font-size, --checkbox--hit-area--size]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules: https://opencomponents.dev/docs/components/checkbox#checklist

It's also published on its own at /raw/docs/components/checkbox.yaml, with every rule from the checklist in place of the rules link. It's a fraction of this page's length, so it's the one to give agents that only need the rules, and not the reasoning behind them.

Every rule has a stable ID, like checkbox/label-toggles, 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 required checkbox with an error:

<Checkbox name="terms" required aria-invalid="true" aria-describedby="terms-error">
  I agree to the terms
</Checkbox>

And here's what it renders:

<label class="checkbox" data-size="md">
  <input
    data-slot="box"
    type="checkbox"
    name="terms"
    required
    aria-invalid="true"
    aria-describedby="terms-error"
  />
  <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">I agree to the terms</span>
</label>
  • The size is mirrored as data-size, which is always there. The <input>'s type is always checkbox.
  • Parts are marked with data-slot and always come in the same order: box, indicator and label. Only the label is optional, and the indicator always holds both marks, which CSS shows one at a time.
  • States change properties and attributes, never elements. Indeterminate is a property of the <input> with no attribute, so it never shows up in the markup, and the <input> is never replaced, 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 <label> wraps the box, so it needs no ID to name it.

Verifiable

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

Unit tests

Every rule that the checklist checks with a unit test has one in Checkbox.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, contrast and target size, and use the keyboard the way an agent would, to check that Space toggles the box and Enter doesn't:

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

test("the terms checkbox toggles with Space and its label, but not with Enter", async ({ page }) => {
  await page.goto("/sign-up");

  const terms = page.getByRole("checkbox", { name: "I agree to the terms" });
  await terms.focus();
  await page.keyboard.press("Space");
  await expect(terms).toBeChecked();
  await page.getByText("I agree to the terms").click();
  await expect(terms).not.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 checkboxes with the keyboard alone, with a screen reader, which should announce each one's name and state, mixed included, 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 checkbox renders without a label or an aria-label, which leaves screen reader users with nothing but "checkbox, not checked".

Prompts

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

Build a Checkbox component for our React design system that meets the Open Components
Checkbox standard: https://opencomponents.dev/raw/docs/components/checkbox.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 against the Open Components Checkbox contract:
https://opencomponents.dev/raw/docs/components/checkbox.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 checkboxes:

Review how our sign-up form uses its checkboxes against the Open Components Checkbox
contract: https://opencomponents.dev/raw/docs/components/checkbox.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/role-and-nameMustComponentIt can be found by its role and label alone, with getByRole("checkbox", { name }), and reports checked, unchecked or mixedWCAG 4.1.2 (A)Unit test
checkbox/deterministic-domMustComponentThe same props render the same markup, with no generated IDs, and states change properties and attributes, never elementsOpen ComponentsUnit test
checkbox/data-attributesShouldComponentThe size is mirrored as data-size, and parts are marked with data-slotOpen ComponentsUnit test
checkbox/dev-warningsShouldComponentIt warns during development when a checkbox has no label and no aria-labelOpen ComponentsUnit test
Copyright © 2026