If a screen reader can't tell what a control is, an AI agent probably can't either. Both rely on the same structure, the accessibility tree the browser builds from your HTML, and a page that's ambiguous there is ambiguous to every reader that depends on it.
Google's web.dev guidance on building agent-friendly websites describes 3 ways agents perceive a page: screenshots, raw HTML, and the accessibility tree. Screenshots need a vision model to interpret, which the guidance calls slow and expensive. Raw HTML is complete but noisy. The tree is the compact one: it drops the styling and keeps what each thing is, what it's called, and what state it's in. That makes it the cheapest place for software to understand your page, and the place where accessibility debt turns into agent failure.
What the accessibility tree actually contains
The browser parses your HTML into the DOM, applies CSS, and then derives a second tree for assistive technology. It exposes that tree through the operating system's accessibility APIs, which is how NVDA, JAWS, VoiceOver, and TalkBack learn what's on screen. Test tools and browser-based agents can read the same tree through the browser's automation interfaces.
Hidden content (display: none, aria-hidden="true") is removed. Layout-only wrappers are flattened or marked as generic. What's left is a set of nodes, each with a few properties:
| Property | The question it answers | Where it comes from | Example |
|---|---|---|---|
| Role | What is this? | The HTML element, or a role attribute | button, link, textbox, checkbox, heading |
| Name | Which one is it? | Text content, <label>, alt, aria-labelledby, aria-label | "Add to cart" |
| State | What condition is it in right now? | Native state, or aria-expanded, aria-checked, aria-invalid | expanded, checked, disabled, invalid |
| Value | What does it hold? | The current value of an input or slider | "40213" |
| Description | Is there anything else to know? | aria-describedby | "Ships in two business days" |
WCAG 2.2 SC 4.1.2 Name, Role, Value (Level A) requires every user interface component to expose a correct name and role, and to expose states and values that people can change. The criterion was written for assistive technology. It now describes what agents need too.
Two patterns that fail both readers
The div that wants to be a button
<div class="btn btn-primary" onclick="addToCart(812)">
<svg class="icon-cart">…</svg>
</div>
In the tree, this is a generic node with no name. It isn't focusable, so a keyboard or screen reader user can't reach it with Tab, and pressing Enter does nothing. An agent reading the tree sees no button at all. An agent reading a screenshot sees a cart icon and has to guess. Google's agent guidance makes the same recommendation accessibility testers have made for years: use <button> and <a> instead of styled <div> and <span> elements.
The placeholder that wants to be a label
<input type="text" placeholder="e.g. 40213">
Browsers fall back to the placeholder when nothing else names a field, so the tree may report a textbox called "e.g. 40213." That's an example, not a label. It disappears visually as soon as someone types, it's usually low-contrast, and it fails SC 3.3.2 Labels or Instructions in most real forms. An agent filling in a shipping form has to infer that "e.g. 40213" means ZIP code. Sometimes it will infer correctly.
The links that all say the same thing
Twelve product cards that each end in "Learn more" give the tree twelve links with identical names. A screen reader user browsing the links list hears "Learn more" twelve times, and an agent choosing which one to follow has to work out the context from the surrounding layout. SC 2.4.4 Link Purpose (In Context) covers this at Level A.
The same markup, fixed
<button type="button" class="btn btn-primary" data-sku="812">
<svg aria-hidden="true" focusable="false">…</svg>
Add to cart
</button>
<label for="zip">ZIP code</label>
<input id="zip" name="postal-code" autocomplete="postal-code"
inputmode="numeric" placeholder="e.g. 40213">
<a href="/packs/trailhead-40/specs">Specifications for the Trailhead 40L pack</a>
Here is roughly what the tree exposes before and after, written in the YAML format Playwright uses for ARIA snapshots:
# Before
- generic
- textbox "e.g. 40213"
- link "Learn more"
# After
- button "Add to cart"
- textbox "ZIP code"
- link "Specifications for the Trailhead 40L pack"
Nothing in the fixed version is clever. A native button gets focus, keyboard activation, and a role for free. The visible label gives the field a stable name, and autocomplete="postal-code" tells browsers, password managers, and agents exactly what the field wants. The link describes where it goes.
How to inspect the tree in five minutes
Chrome DevTools
- Open the Elements panel, select an element, and open the Accessibility pane in the sidebar. You'll see the element's place in the accessibility tree, its ARIA attributes, and its computed properties. The Name entry lists every possible source in order of precedence and strikes through the ones that lost, which is the fastest way to answer "why does the screen reader say that?"
- In the same pane, tick Enable full-page accessibility tree and reload DevTools when prompted. A new button at the top of the Elements panel switches between the DOM tree and the full accessibility tree for the page.
- Walk your most important journey in that view. Every control should have a role you'd expect and a name a stranger would understand.
Firefox has an equivalent Accessibility Inspector, with a built-in check for common issues.
Playwright ARIA snapshots
Playwright can capture the tree as YAML and assert against it in a test. Its older page.accessibility.snapshot() API is deprecated, so use ARIA snapshots instead:
import { test, expect } from "@playwright/test";
test("product page is readable by role and name", async ({ page }) => {
await page.goto("/packs/trailhead-40");
console.log(await page.getByRole("main").ariaSnapshot());
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- heading "Trailhead 40L pack" [level=1]
- button "Add to cart"
- textbox "ZIP code"
`);
await page.getByRole("button", { name: "Add to cart" }).click();
});
A useful rule: if getByRole with a name can find your control, a screen reader user and an agent probably can too. If your tests have to fall back to CSS classes or nth() to click something, write that down as a finding.
States are where agents get lost
Agents work in a loop: act, observe, decide. The observing step depends on state. If a filter panel opens but the toggle doesn't say it's expanded, or a form rejects an email address but nothing in the tree says the field is invalid, the agent can't tell whether its action worked. Screen reader users hit exactly the same wall.
<button type="button" aria-expanded="true" aria-controls="filters">Filters</button>
<input id="email" type="email" aria-invalid="true" aria-describedby="email-error">
<p id="email-error">Enter an email address with an @ sign.</p>
<p role="status">3 items in your cart</p>
Update aria-expanded when the panel opens and closes, set aria-invalid and point to the error message when validation fails, and announce cart or result counts through a status region. The ARIA Authoring Practices Guide has tested patterns for each.
Structured data and link text: the layer above the tree
The accessibility tree describes controls and structure. It doesn't say that a page is a product, what it costs, or whether it's in stock. Structured data does, in a vocabulary crawlers and agents already know:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Trailhead 40L pack",
"sku": "TH-40",
"offers": {
"@type": "Offer",
"price": "149.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
}
}
</script>
Keep it consistent with what's visible on the page; structured data that disagrees with the page is worse than none. And write link text that would make sense read aloud in a list, because for screen reader users and agents alike, that is often how links are compared.
A short list for this sprint
- Open the full-page accessibility tree on your three most important journeys and look for generic nodes doing a button's job.
- Replace
<div>and<span>controls with<button>and<a>. - Give every form field a visible
<label>and anautocompletevalue where one applies. - Add
getByRoletests for the controls that make you money.
If you want an outside view, our Agent-Readiness Audit tests how agents and crawlers read your key journeys, and our remediation team fixes what it finds in your source code.
