AccessibilityGENERALPLATFORM-SPECIFICBROWSER-SPECIFIC

The Accessibility Tree

The browser derives a second tree from the DOM — role, name, state and relationships — and hands it to the platform. That tree, not your markup and not your pixels, is what assistive technology reads.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

What does assistive technology actually read, and where does it come from?

The user intent

Someone who cannot see the screen, or cannot use a mouse, wants the same things everyone else wants: to know what is on this page, which parts are interactive, what state they are in, and what changed after they acted.

The obvious build

A screen reader reads the page out loud. If the text is on screen and the visual design is clear, the page will read clearly too — and alt text on images covers the rest.

Why it breaks

Nothing reads the page. Assistive technology reads the accessibility tree — a separate structure the browser computes — and an element with no semantics appears there as a generic node with no role, no name and no indication that clicking it does anything.

How it breaks in a real browser
  • Nothing reads the page. Assistive technology reads the accessibility tree — a separate structure the browser computes — and an element with no semantics appears there as a generic node with no role, no name and no indication that clicking it does anything.
  • An icon-only button built from a div and a background image has no name at all. Some screen readers announce "button" with silence after it; some skip it; some read the class name; none of them tell the user what it does.
  • Visual proximity is not a relationship. A label sitting next to an input is a layout fact. Only label for, wrapping, aria-labelledby or an equivalent makes it the input's name in the tree.
  • State expressed in class names is invisible. class="is-checked" styles a pixel; checked or aria-checked is what reaches the tree. A control can look selected and be exposed as unselected, and nobody sighted will ever notice.
  • Hiding is not one thing. display: none and visibility: hidden remove a subtree from the tree; opacity: 0, clip-path, transform: translateX(-100%) and off-screen positioning do not. An "invisible" carousel slide or a closed drawer is frequently still read, and still focusable (Focus Management).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • From one DOM plus computed style the browser derives two projections: the render tree that becomes pixels, and the accessibility tree that becomes a platform accessibility object graph. Same source, different consumers, and they diverge exactly where semantics are missing (The DOM Is Not Your HTML).
  • Each node in that tree carries a role (what kind of thing it is), a name (what it is called), a description (extra detail), states and properties (checked, expanded, disabled, required, invalid, busy), and relationships (labelled by, described by, controls, owns, parent/child, position in set).
  • The role is implicit from the element (button, nav, h2, input type="checkbox") unless an explicit role attribute overrides it. The override changes the tree only — never the behaviour (Semantics Before ARIA).
  • The name is computed, not stored: an ordered algorithm walks a fixed list of sources and takes the first that yields non-empty text. This is why aria-label silently beats the visible text of a button, and why fixing "it reads the wrong thing" is usually about which source won, not about adding another one.
  • The tree is pruned and flattened. Hidden subtrees are dropped, role="presentation" nodes are removed while their children are kept, and purely stylistic wrappers are collapsed. The tree is generally much smaller than the DOM.
  • The browser then maps every node onto the operating system's accessibility API — UI Automation or IAccessible2 on Windows, the AX API on macOS and iOS, AccessibilityNodeInfo on Android — and fires platform events when it changes. The screen reader consumes *that*, and adds heuristics of its own on top.

What this makes the browser do

And which of it is avoidable.

  • Style must be computed before the tree can be built, because visibility, display, generated content and aria-hidden inheritance all depend on it. Accessibility is downstream of the same style pass as layout (The Rendering Pipeline).
  • Every DOM mutation is also a candidate accessibility-tree mutation: the browser patches the affected nodes, recomputes names that depended on them, and fires platform events. A thousand-row list swap is a thousand-node tree rebuild plus an event storm the AT has to absorb.
  • In a multi-process browser the tree is computed in the renderer and mirrored to the browser process, which owns the connection to the platform API. That serialisation is real work, which is why gratuitously churning the DOM has an accessibility cost as well as a rendering one (What a Mutation Costs).
  • Avoidable work: recomputing names for subtrees that did not change (usually caused by replacing nodes instead of updating them), and live regions scoped so widely that every unrelated re-render produces an announcement (Live Regions and Announcement).

One document, two projections

The browser does not have a single internal model of the page that both drives pixels and drives assistive technology. It has one source — the DOM plus computed style — and derives two structures from it. The render tree exists to produce frames and knows nothing about meaning. The accessibility tree exists to produce a semantic description and knows nothing about pixels.

This is why "it looks right" and "it reads right" are independent claims, and why the second one has to be checked separately. Two components can be visually identical and have completely different entries in the tree: one a button with a name and a pressed state, the other a generic node the tree may not even keep.

The tree is also not the end of the pipeline. The browser maps it onto the operating system's accessibility API, and the screen reader builds its own model from that, applying heuristics that vary by product. Your influence stops at the tree; everything after it is somebody else's software making the best of what you provided.

From markup to spoken output
semantics + stylemirrored + eventsHTML + ARIA attributesDOM + computed styleRender tree → layout → paintAccessibility tree (role, name, state, relations)PixelsPlatform accessibility API (UIA / AX / AccessibilityNodeInfo)Screen reader, switch access, voice control, brailleA person who now knows what this is
UserLLMAgentToolDataDecisionHumanGuardrail

Role, name, state — the contract every control owes

Three questions have to be answerable for every interactive thing on the page: what kind of control is this, what is it called, and what condition is it in. A component that cannot answer all three is broken in the tree even when it is pixel-perfect on screen.

The pair below is the canonical example, because the two render identically and the second one costs nothing extra to write. The difference is that the second one participates in the platform: it is in the tab order, it responds to Enter and Space, it exposes a pressed state that updates, and it has a name that a voice-control user can say out loud.

accessibility specIcon-only toggle button (favourite, mute, bookmark)Specification for an icon-only toggle

semantics A native button with type="button" and aria-pressed reflecting the current state. The icon is aria-hidden; the name comes from visually-hidden text or aria-label that matches the tooltip.

Tab / Shift+TabMoves focus to and from the button. Native button is in the tab order with no tabindex at all.
EnterActivates. The browser does this; no key handler needed.
SpaceAlso activates, on key-up. A div with a click handler does neither.
Focus
  • Focus stays on the button through the state change — never move focus as a side effect of toggling.
  • The focus indicator must be visible against both the pressed and unpressed backgrounds (Contrast, Colour and Motion).
Announces
  • On focus: the name, the role "button", and the pressed state — "Add to favourites, toggle button, pressed".
  • On activation: the state change alone. aria-pressed flipping is announced by the screen reader; adding a live region on top of it produces a double announcement.

usually broken by Swapping the *name* to reflect state ("Add to favourites" becoming "Remove from favourites") instead of flipping aria-pressed. The control now changes identity under the user, voice-control targets move, and the pressed state is never exposed at all.

The same button, twice
1<!-- Accessibility tree: role=generic, no name, no state, not focusable.
2 A mouse can use this. Nothing else can. -->
3<div class="btn is-active" onclick="toggleFavourite()">
4 <span class="icon icon-star"></span>
5</div>
6
7<!-- Accessibility tree: role=button, name "Add to favourites",
8 pressed=true, focusable, activated by Enter and by Space. -->
9<button type="button" aria-pressed="true" onclick="toggleFavourite()">
10 <svg aria-hidden="true" focusable="false"><use href="#star" /></svg>
11 <span class="visually-hidden">Add to favourites</span>
12</button>
13
14<style>
15 /* Visually hidden, still in the accessibility tree.
16 display:none and visibility:hidden would remove it from both. */
17 .visually-hidden {
18 position: absolute;
19 width: 1px; height: 1px;
20 margin: -1px; padding: 0; border: 0;
21 clip-path: inset(50%);
22 overflow: hidden;
23 white-space: nowrap;
24 }
25</style>

The aria-hidden on the SVG is not optional decoration: without it, some engines compute the button's name from the icon's title or from nothing at all, and the deliberate name below it is ignored.

Where the name comes from

SPEC-EVOLVINGThe accessible name computation is a living specification and the edge cases genuinely move — naming prohibitions on generic roles, and the treatment of title and of hidden referenced nodes, have all been tightened in recent revisions. The ordering above is stable; the boundary cases should be read from the current spec, not memorised from a lesson.

The accessible name is computed by walking an ordered list of sources and taking the first that produces non-empty text. Almost every "it reads the wrong thing" bug is a source higher in the list than the one you were editing, quietly winning.

The order below is the practical shape of the algorithm for a typical control; the full specification has more cases, and which native attribute applies depends on the element (alt for images, label for form controls, caption for tables, legend for a field set). The rule that transfers is: more specific author intent wins, content is a fallback, and `title` is the last resort.

Accessible name computation, in the order the browser tries it
  1. 1
    aria-labelledby

    Concatenates the text content of the referenced elements, in the order listed. Wins over everything else.

    fails by A stale or misspelled id reference resolves to nothing, and the browser falls silently to the next source — no error anywhere.

  2. 2
    aria-label

    Uses the attribute string directly.

    fails by Overriding visible text with something different, which breaks voice control and confuses anyone using speech and sight together. Also ignored entirely on roles where naming is prohibited (The Rules of ARIA).

  3. 3
    Native host-language label

    The element's own mechanism: label for, a wrapping label, alt, caption, legend, figcaption.

    fails by A placeholder used instead of a label — it is not a name source in most engines, and it disappears the moment the user types (Input Types, Inputmode and Autocomplete).

  4. 4
    Subtree content

    The flattened text of the element's descendants, which is how <button>Save</button> gets its name for free.

    fails by Icon-only content, so there is no text to flatten; or an aria-hidden wrapper that removes the only text there was.

  5. 5
    title attribute

    Last-resort fallback, and also the source of the description if a name was already found.

    fails by Never shown on touch, requires hover on desktop, announced inconsistently across screen readers. Fine as a supplement, never as the name.

  6. 6
    Nothing

    The node is exposed with an empty name.

    fails by The AT falls back to its own heuristics — reading the src filename, the id, or simply "button" — and the user is guessing.

Check the result rather than the input: Chrome's Accessibility pane lists the computed name and the source it came from, which turns this from a memory exercise into a two-second observation.

How to build it

Most important first.

  • Pick the element whose implicit role is the role you want. The implicit role comes bundled with focusability, keyboard activation and default actions that ARIA cannot give you (What Native Elements Already Do).
  • Give every interactive control an accessible name, and make it match the visible label. Voice-control users speak the visible text; if aria-label says something else, "click Save" fails on a button that plainly says Save.
  • Express state through the state mechanism — the native attribute where one exists, the matching aria-* property where one does not — and drive the styling from that state rather than from a parallel class.
  • Use the relationships the platform already has: label for, fieldset/legend, th scope, caption, aria-describedby for hint and error text (Errors People Can Actually Perceive).
  • Hide decoration explicitly. Inline SVG icons need aria-hidden="true", and in some engines focusable="false" as well; a decorative image needs alt="", not a missing alt.
  • Inspect the tree instead of imagining it. Chrome's Accessibility pane and its full-page accessibility tree, Firefox's Accessibility panel, and the macOS Accessibility Inspector all show you what you actually built (A Mental Model of the Devtools).

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • Role, name and state is the minimum contract for every interactive element. If you cannot state all three for a component you built, its accessibility tree entry is incomplete by construction.
  • A correct tree is not operability. Operability is keyboard reach and activation (Keyboard Operability); knowing where you are is focus management (Focus Management); knowing what changed is announcement (Live Regions and Announcement).
  • Screen readers are not the only consumer. Switch access, voice control, braille displays, screen magnifiers and browser reader modes all read the same tree, and they need different parts of it: voice control lives on names, magnifiers on focus and bounds, switch access on the set of operable nodes.
  • Reading order follows the accessibility tree, which follows DOM order — not visual order. CSS that reorders content visually (order, grid-area, direction) leaves the reading and tab order where the DOM put them, which is a defect the moment the two disagree.

What can go wrong

Failure modes
  • The name comes from a source you did not intend — title on a link, an aria-label added years ago, or an alt text on an icon inside a button — and now the announced name and the visible name disagree.
  • title used as the only name. It is announced inconsistently, has no touch equivalent, is invisible to keyboard users until the mouse hovers, and is the lowest-priority source for a reason.
  • An explicit role flattens or reparents children in ways you did not expect: role="presentation" on a table strips its rows and headers out of the tree, taking the column relationships with them.
  • Emoji and icon fonts read literally. A glyph used as a status indicator is announced as its Unicode name, or as a private-use character, or not at all.
  • The tree is correct and the component is still unusable, because nothing in it is reachable by keyboard. Role, name and state are necessary and nowhere near sufficient (Keyboard Operability).
  • Automated checks pass. They verify things that are machine-decidable — a missing name, a bad attribute, a broken id reference — and cannot tell that the name is wrong, that the reading order is nonsense, or that the flow is impossible to complete.
What can arrive out of order
  • The accessibility tree is updated after the DOM mutation that caused it, not synchronously with it. Code that mutates and then immediately asserts what the AT sees is reading a value that has not settled yet.
  • Focusing a node and removing it in the same task loses focus entirely: the browser resolves the removal, focus falls to body, and the AT reports nothing where the user just was.
  • An announcement and a focus move issued in the same frame race each other, and which one wins is a property of the screen reader, not of your code (Live Regions and Announcement).
Security
  • The accessibility tree exposes what the DOM already exposes; it is not a new trust boundary and not a place to hide anything. Visually-hidden text is fully readable in the page source.
  • It is a genuine surface for content injection: an attacker-controlled string in an aria-label is spoken with the same authority as your own copy, and injected markup can silently restate what a control claims to do (Cross-Site Scripting).
  • aria-disabled is a statement about the interface, not a control. It does not block events and does not stop a request. Anything that matters is enforced by the server (Authorization-Aware UI).
  • Screen-reader-only text sometimes leaks internal detail — record ids, internal state names, admin-only hints — because nobody reviewed it as visible copy. It is visible copy.
Misreads
  • "The screen reader reads the DOM." It reads a derived tree, pruned and flattened, through a platform API, with its own heuristics on top. Several things present in the DOM are simply not in it.
  • "Accessibility is alt text." alt computes a name for one element type. Role, state, relationships, focus order, keyboard operation and announcement are all still open questions.
  • "aria-label makes anything accessible." It adds a name to a node that may still have no role, no focusability and no keyboard behaviour — and on many roles it is ignored outright (The Rules of ARIA).
  • "The automated audit is green, so the page is accessible." Automated tooling reliably catches a minority of real defects. It cannot judge whether a name is correct, whether the flow is completable, or whether the announcement made sense.

Measuring it, and what changes in the field

How you would see this
  • Chrome DevTools: the Accessibility pane on a selected element shows its computed name and the source that produced it, plus the full ARIA state; the full-page accessibility tree view shows the pruning.
  • Firefox DevTools: the Accessibility panel renders the tree with a "check for issues" pass over contrast, keyboard and text labels.
  • The macOS Accessibility Inspector and Windows Accessibility Insights show the platform-level object, which is one step further than the browser's own view and is where browser/AT disagreements become visible.
  • The only measurement that settles a dispute is a real screen reader on a real page, driven by the keyboard (Accessibility Testing).
Slow device, slow network, large data, old tab
  • A very large DOM produces a very large tree, and the rotor and heading lists a screen-reader user navigates by become long enough to be useless. Landmarks and headings are the index; without them the page is a wall (Document Structure and Reading Order).
  • Virtualised lists break the tree's idea of set size: the AT reports "item 3 of 20" when there are 4,000, unless you supply aria-setsize and aria-posinset yourself (List Virtualization).
  • Shadow DOM participates in the tree, but aria-labelledby and friends cannot cross the boundary by id — the reference has to resolve in the same tree (Shadow DOM and the Composed Tree).
  • Cross-origin iframes contribute their own subtree; focus and announcement move between documents, and a live region in one is invisible to the other.
  • On a slow device the tree updates are queued behind the same main-thread work as everything else, so a long task delays announcements and focus moves as surely as it delays paint (Long Tasks).
What this costs
  • Expressing semantics constrains the design. A real select, a real dialog and a real button come with platform behaviour and platform styling limits, and someone will want a look the element does not offer. That negotiation is the work; skipping it by using a div moves the cost onto users.
  • An explicit role is a promise about behaviour that you are now responsible for keeping, forever, including in the refactor two years from now that nobody accessibility-tests.
  • Reading the tree in devtools is slower than reading the markup, and it is the only way to know what you built. There is no shortcut that is also honest.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe role/name/state model and the name computation order are specified (ARIA and the accessible name computation) and implemented by Blink, Gecko and WebKit alike. Where engines differ it is in pruning details and in how quickly the tree is updated, not in the model.
  • PLATFORM-SPECIFICThe same tree is mapped onto different platform APIs — UI Automation/IAccessible2 on Windows, the AX API on macOS and iOS, AccessibilityNodeInfo on Android — and screen readers add their own heuristics: NVDA and JAWS on Windows differ from each other on the same page, and VoiceOver on macOS often announces group boundaries neither of them mentions.
  • BROWSER-SPECIFICDevtools support is uneven: Chrome shows the computed name plus the source that produced it and a full-tree view, Firefox shows a tree with an issues audit, and Safari expects you to use the separate macOS Accessibility Inspector for anything beyond the basics.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Securityxss
Domains that do not exist yet
  • Testing & Reliability Engineering — how to test a derived structure like the accessibility tree: snapshotting roles and names in component tests, and why the assertion "has an accessible name" is worth more than any DOM-shape assertion.