Input

Agentic Experience (AX)

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

Finally, let's look at how agents read, fill in and check an input. 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. This is how they see some of the examples on this page:

- textbox "Full name": Ada Lovelace
- textbox "Username" [disabled]: ada
- textbox "Account ID": ACME-4821
- textbox "Email address" [invalid]: ada@
- searchbox "Search"
- textbox "Password"
- button "Show password"

An input without a label is a textbox with no name, so an agent can't tell an email address from a search, any more than a screen reader user can. With a role and a name, an agent can find the input and fill it in:

await page.getByRole("textbox", { name: "Email address" }).fill("ada@example.com");

A password input is the odd one out. ARIA doesn't define a role for it, so tools disagree on what it is: Playwright reports it as a textbox, as above, while Testing Library doesn't find it by that role at all. Finding it by its label, with getByLabel("Password") or getByLabelText("Password"), works everywhere.

Described

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

component: Input
summary: Takes a single line of text, like a name, an email address or a search. Its label names it.
standard: https://opencomponents.dev/docs/components/input
element: input # inside a div, which draws the field and takes class and style
role: textbox # searchbox, with type="search". ARIA has none for type="password"
props:
  size: { type: [xs, sm, md, lg, xl], default: md }
  type: { type: [text, email, password, search, tel, url], default: text }
  disabled: { type: boolean, default: false }
  value: { type: string } # when it's bound. Without it, the input keeps its own
slots:
  leading: An icon or a prefix before the value, hidden from assistive technologies.
  trailing: An icon or a suffix after the value, hidden from assistive technologies.
  actions: Buttons that act on the value, which Tab reaches after the input.
events:
  input: Event, from the <input>. Fires as people type.
  change: Event, from the <input>. Fires when people commit a change, like when they leave it.
states:
  hover: ":hover, in @media (hover: hover)"
  focus: ":has(> input:focus-visible)"
  disabled: ":has(> input:disabled)"
  read-only: ":has(> input[readonly])"
  invalid: ":has(> input[aria-invalid=true]), never :invalid"
keyboard:
  Tab: Moves focus to the input, then to its actions.
  Enter: Submits its form.
parts: [leading, control, trailing, actions] # data-slot, in this order. The control is the <input>
tokens:
  theme: ["--color--neutral-text", "--color--error-text", "--color--focus"]
  variables: [--input--height, --input--padding, --input--gap, --input--font-size, --input--icon--size, --input--radius, --input--border-color]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules: https://opencomponents.dev/docs/components/input#checklist

It's also published on its own at /raw/docs/components/input.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 input/visible-label, 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 with an error:

<Input
  id="email"
  type="email"
  autocomplete="email"
  aria-invalid="true"
  aria-describedby="email-error"
/>

And here's what it renders:

<div class="input" data-size="md">
  <input
    data-slot="control"
    type="email"
    id="email"
    autocomplete="email"
    aria-invalid="true"
    aria-describedby="email-error"
  />
</div>
  • The size is mirrored as data-size, which is always there. The type is the <input>'s own type, which is always set, text included.
  • Parts are marked with data-slot and always come in the same order: leading, control, trailing and actions. Only the control is always there.
  • States change attributes, never elements. The <input> is never replaced, so focus, the caret, what people have typed and the screen reader's position all survive every state change, an error included.
  • 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. You give the input its id, which the Field will do for you.
  • The type is always a prop, and never guessed from the input's name or autocomplete.

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 Input.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 autocomplete values, and fill in the form the way an agent would, to check what happens when a value is wrong:

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

test("the newsletter form says what's wrong with an email address", async ({ page }) => {
  await page.goto("/newsletter");

  const email = page.getByRole("textbox", { name: "Email address" });
  await email.fill("ada@");
  await page.getByRole("button", { name: "Subscribe" }).click();

  await expect(email).toBeFocused();
  await expect(email).toHaveAttribute("aria-invalid", "true");
  await expect(email).toHaveAccessibleDescription(/in the correct format/);

  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, on a phone, where you can check the keyboards, autofill and zoom, 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 an input renders without a label, which is the most common input bug out there.

Prompts

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

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

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