Field

Agentic Experience (AX)

How agents read, fill in and check a field, from its accessibility tree to its contract, its DOM and its tests.

Finally, let's look at how agents read, fill in and check a field. 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, values and states. The Field adds nothing to it but text: the control keeps its own role, and gets its name from the label and its description from the hint and the error. This is how they see the examples under Controls:

- text: Email address
- paragraph: We'll only use it to send your receipt
- textbox "Email address" [invalid]
- paragraph: Enter your email address
- text: Bio
- paragraph: A sentence or two about you, for your profile
- textbox "Bio"
- text: 200 characters left
- checkbox "I agree to the terms" [invalid]
- text: I agree to the terms
- paragraph: You can read them before you sign up
- paragraph: Agree to the terms to create an account
- text: Price alerts
- switch "Price alerts"
- paragraph: We'll email you when the price drops
- group "How can we contact you?":
  - text: How can we contact you?
  - checkbox "Email" [invalid]
  - text: Email
  - checkbox "Text message" [invalid]
  - text: Text message
  - checkbox "Phone call" [invalid]
  - text: Phone call
- paragraph: Pick at least one
- paragraph: Choose how we can contact you
- radiogroup "Delivery" [invalid]:
  - text: Delivery
  - paragraph: Express arrives the next working day
  - paragraph: Choose a delivery option
  - radio "Standard"
  - text: Standard
  - radio "Express"
  - text: Express

A label, a hint or an error that isn't linked to its control is only text next to it, so an agent, like a screen reader user, can't tell which control it belongs to. With the Field, the control is found by its name, and its description and invalid state say what's wrong with it:

const email = page.getByRole("textbox", { name: "Email address" });
await expect(email).toHaveAccessibleDescription(/send your receipt/);
await expect(email).toHaveAttribute("aria-invalid", "true");

Described

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

component: Field
summary: Puts a label, a hint and an error message around one form control, and links them to it with its id, aria-describedby and aria-invalid. It renders nothing itself around a Radio Group, which takes its hint and error as props.
standard: https://opencomponents.dev/docs/components/field
element: div # around the control, with data-control naming its kind. None around a Radio Group
props:
  id: { type: string } # the control's. Without it, the control's own id, then its name. The hint's and error's IDs add -hint and -error
  label: { type: string } # for a text control or a Switch. A Checkbox and the groups name themselves
  hint: { type: string }
  error: { type: string } # while it's set, the control is invalid
  required: { type: boolean, default: false } # the control's native required, never a Checkbox Group's
  disabled: { type: boolean, default: false }
  size: { type: [xs, sm, md, lg, xl] } # also goes to a component control, never a native one. Without it, the control's own, then md
slots:
  default: One control, an Input, a Textarea, a Checkbox, a Switch, a Checkbox Group, a Radio Group or a native input, textarea or select.
states:
  required: "[required] on the control, or on each radio in a Radio Group"
  disabled: ":has(> :disabled, > * > [data-slot=control]:disabled), the control only"
  invalid: "[aria-invalid=true] on the control, never :invalid"
parts: [label, hint, error] # data-slot. In the DOM, around a text control: label, hint, control, error. Around a Switch: label, control, hint, error. Around a Checkbox or a Checkbox Group: control, hint, error
tokens:
  theme: ["--color--neutral-text", "--color--error-text"]
  variables: [--field--gap, --field--font-size, --field--column-gap, --field--indent]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules: https://opencomponents.dev/docs/components/field#checklist

It's also published on its own at /raw/docs/components/field.yaml, with every rule from the checklist in place of the rules link. Its controls follow their own contracts too, like the Input's, so give agents both.

Every rule has a stable ID, like field/described, 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 an email input in a Field with a hint and an error:

<Field id="email" label="Email address" hint="We'll only use it to send your receipt" error="Enter your email address" required>
  <Input type="email" autocomplete="email" />
</Field>

And here's what it renders:

<div class="field" data-size="md" data-control="text">
  <label data-slot="label" for="email">Email address</label>
  <p data-slot="hint" id="email-hint">We'll only use it to send your receipt</p>
  <div class="input" data-size="md">
    <input
      data-slot="control"
      type="email"
      autocomplete="email"
      id="email"
      aria-describedby="email-hint email-error"
      aria-invalid="true"
      required
    />
  </div>
  <p data-slot="error" id="email-error">Enter your email address</p>
</div>
  • The size is mirrored as data-size, and the kind of control as data-control, which is text, checkbox, switch or checkbox-group. Both are always there.
  • The parts are marked with data-slot, and always come in the same order for each kind of control, as in the contract. The hint and the error are only there when they're set.
  • States change attributes, and add or remove the error, but never replace an element. The control is never re-created, so focus, the caret, what people have typed and the screen reader's position all survive an error showing up.
  • There's nothing random or generated, like IDs or timestamps. The IDs come from the Field's id, or the control's id or name, so the markup is the same on the server, on the client and in every snapshot.
  • Around a Radio Group, the Field renders no element, and the group's markup is the one on its page, with the Field's hint and error in it.

Verifiable

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

Unit tests

Every rule that the checklist checks with a unit test has one in Field.test.ts, which you'll find under Reference implementation. They mount the real Input, Textarea, Checkbox, Switch, Checkbox Group and Radio Group, so they check the Field against the controls it wraps. 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 send a form the way an agent would, to check what happens when it fails:

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

test("the sign-up form says what's wrong, and takes people to the first error", async ({ page }) => {
  await page.goto("/sign-up");

  await page.getByRole("button", { name: "Create account" }).click();

  const name = page.getByRole("textbox", { name: "Full name" });
  await expect(name).toBeFocused();
  await expect(name).toHaveAttribute("aria-invalid", "true");
  await expect(name).toHaveAccessibleDescription("Enter your full name");
  await expect(page.getByRole("status")).toHaveText(/Couldn't create your account/);

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

Some things still need a human, so try your forms with the keyboard alone, with a screen reader, to hear the hint and the error read after each control's name, on a phone, 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 when a Field has no id to link its parts with, when it gets a label around a control that names itself, when it gets a native checkbox or radio, and when it holds anything other than one control.

Prompts

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

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

Review how our sign-up form uses its fields against the Open Components Field
contract: https://opencomponents.dev/raw/docs/components/field.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
field/role-and-nameMustComponentIts control can be found by its role and the Field's label, or by its own label or legend, with getByRoleWCAG 4.1.2 (A)Unit test
field/deterministic-domMustComponentThe same props render the same markup on the server and the client, with no generated IDs, and states never replace the controlOpen ComponentsUnit test
field/data-attributesShouldComponentThe size is mirrored as data-size, the kind of control as data-control, and parts are marked with data-slotOpen ComponentsUnit test
field/dev-warningsShouldComponentIt warns during development when it has no id, a label its control doesn't take, a native checkbox or radio, or other than one controlOpen ComponentsUnit test
Copyright © 2026