ESC

Type to search the knowledge base.

Accessibility Tree Overview

How browsers build the accessibility tree from DOM and CSS — roles, names, states, what’s pruned, and how to inspect it in DevTools.

intermediate3 min read
  • html
  • accessibility-tree
  • a11y

Screen readers and other assistive tech don’t read your raw HTML string. They walk the accessibility tree — a derived structure of roles, names, values, and states built from the DOM, certain CSS, and ARIA. If a node isn’t in that tree, users who rely on AT may never know it exists.

Docs: MDN accessibility tree, Chrome a11y tooling, ARIA in HTML.

From DOM to a11y tree

Rough pipeline:

  1. Parse HTML → DOM
  2. Apply styles → determine visibility/display
  3. Compute accessible objects with roles and properties
  4. Expose via platform APIs (macOS AX, Windows UIA/IA2, etc.)
<button type="button" aria-expanded="false" aria-controls="menu">
  Menu
</button>
<ul id="menu" hidden>…</ul>

The button appears as a button role, name “Menu”, state collapsed. The hidden list is omitted or marked hidden depending on the hidden/CSS path.

What gets pruned

Typically not exposed as interesting accessible nodes:

  • Elements with display: none or visibility: hidden (with nuances)
  • hidden attribute / aria-hidden="true" (descendants ignored for AT — careful)
  • Purely decorative elements with proper patterns (alt="", role="presentation" where valid)
  • Some presentational children of certain roles
<!-- Decorative -->
<img src="/swirl.svg" alt="" />

<!-- Mistakenly hidden from AT -->
<div aria-hidden="true">
  <button type="button">Checkout</button> <!-- disaster -->
</div>

Never put focusable controls inside aria-hidden="true" regions.

Name, role, value

Every interactive control needs a role (often native), an accessible name, and relevant states/values.

Piece Sources
Role Native element (button, a, input) or ARIA role
Name Content, <label>, aria-label, aria-labelledby, alt, …
State disabled, aria-expanded, aria-checked, validity, …
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" />

Name comes from the label association — better than aria-label alone for visible text.

Inspecting

  • Chrome/Edge: Elements → Accessibility pane
  • Firefox: Accessibility Inspector
  • Safari: Develop → Show Web Inspector → Node → Accessibility

Verify:

  1. Is the control present?
  2. Is the name what you expect?
  3. Are states updated when the UI changes?
  4. Does the order match a sensible reading order?

HTML first, ARIA second

Native elements ship correct roles and keyboard behavior:

<!-- Prefer -->
<button type="button">Delete</button>

<!-- You must rebuild name, role, keyboard, disabled -->
<div role="button" tabindex="0">Delete</div>

ARIA can fix gaps; it can’t put back default keyboard semantics you threw away without extra JS.

Interview out-loud

“The accessibility tree is the browser’s AT-facing structure of roles, names, and states derived from DOM and styles. Hidden CSS and aria-hidden prune nodes. I inspect it in DevTools when a control isn’t announced, prefer native elements for free roles, and ensure every interactive control has a computed accessible name.”

Footguns

  1. Icon-only buttons with no name.
  2. aria-hidden on live regions or focused ancestors.
  3. CSS that visually shows content but removes it from the tree (or the reverse with off-screen hacks).
  4. Duplicate names from both visible label and aria-label.
  5. Assuming the DOM order always matches the a11y order with CSS grid reordering — test.

Further reading

Related guides