Accessibility Tree

TL;DR / The accessibility tree is a parallel representation of the DOM that browsers construct for assistive technologies, containing only semantically meaningful nodes with computed roles, names, states, and relationships.

How It Works

 ┌────────────┐              ┌────────────────┐
 │  DOM Tree  │──────┐       │ Accessibility  │
 └────────────┘      └──────→│      Tree      │
                             │                │
                             └────────────────┘


   ┌───────┐                 ┌─────────────┐
   │ <div> │────────────────→│ role: group │
   └───────┘                 └─────────────┘


   ┌──────────┐              ┌──────────────┐
   │ <button> │──────┐       │ role: button │
   └──────────┘      └──────→│ name: Submit │
                             │              │
                             └──────────────┘


   ┌────────┐
   │ <span> │                (excluded: no semantic role)
   └────────┘

Edit diagram

The accessibility tree is a browser-constructed data structure that serves as the interface between web content and assistive technologies (screen readers, switch access devices, voice control software). While the DOM represents the full document structure, the accessibility tree distills it into semantically meaningful nodes that convey purpose, state, and relationships to users who cannot perceive the visual rendering.

Every node in the accessibility tree has four core properties. Role identifies what the element is: button, link, heading, textbox, checkbox, tree, grid, and dozens more. Roles come from HTML semantics (<button> = role button, <nav> = role navigation) or explicit ARIA (role="tabpanel"). Name is the accessible label: computed from content (button text), attributes (aria-label, alt, title), or label associations (<label for>). State/Properties include checked, expanded, disabled, required, aria-selected, aria-pressed, and others conveying dynamic state. Relationships connect elements: aria-labelledby, aria-describedby, aria-controls, aria-owns.

Not every DOM node appears in the accessibility tree. The browser applies pruning rules: elements with no semantic role (generic <div>, <span> without ARIA) are typically flattened -- their children are promoted to the parent. Elements hidden via display: none, visibility: hidden, or aria-hidden="true" are excluded entirely (along with their entire subtree for aria-hidden). Purely presentational elements (role="presentation" or role="none") are removed, promoting their children.

The name computation algorithm (formally specified in the Accessible Name and Description Computation spec) resolves the name through a priority chain: aria-labelledby (highest priority, follows ID references and concatenates text), aria-label, native labeling (<label>, alt attribute), element content (text nodes, recursively for named-from-content roles), and title attribute (lowest priority). Understanding this priority chain is essential because conflicting sources (an aria-label and inner text) do not concatenate -- the higher-priority source wins.

Computed roles are not always what you expect. A <div> with an onclick handler has no implicit role -- screen readers present it as a generic container, not a button. An <a> without an href attribute has no role, despite looking like a link. A <table> used for layout with role="presentation" removes the table semantics entirely. The accessibility tree reflects the computed result, not the developer's visual intent.

Chrome DevTools exposes the accessibility tree in the Elements panel (Accessibility sidebar) and through the dedicated Accessibility panel. The full tree view shows the parallel structure, letting you inspect each node's computed role, name, state, and properties. Firefox's Accessibility Inspector provides similar functionality with an additional audit for common issues (missing labels, incorrect roles, contrast violations).

ElementInternals in custom elements enables proper accessibility tree participation. When building web components, this.internals = this.attachInternals() provides methods like internals.role, internals.ariaLabel, and form-associated callbacks. Without ElementInternals, custom elements appear as opaque generic containers in the accessibility tree.

The AOM (Accessibility Object Model) proposal aims to provide direct JavaScript access to the accessibility tree: reading computed properties, reacting to accessibility events, and building virtual accessibility trees for canvas-based UIs. Currently partially implemented, the property reflection part (element.role, element.ariaLabel as IDL attributes rather than content attributes) is available in all modern browsers.

Testing accessibility tree output is critical. Automated tools (axe-core, Lighthouse accessibility audit) catch a subset of issues -- missing names, invalid roles, color contrast. Manual testing with an actual screen reader (VoiceOver, NVDA, JAWS) reveals interaction patterns and announcement sequences that automated tools cannot evaluate: focus management, reading order, live region announcements, and modal trapping.

Gotchas

  • role="presentation" on a focusable element is ignored -- if an element is focusable (via tabindex or native focusability), the browser preserves its role to maintain keyboard accessibility, silently overriding your presentation role
  • aria-hidden="true" hides the entire subtree -- applying it to a parent removes all children from the accessibility tree, including focusable elements; this creates a keyboard trap where users can focus elements they cannot perceive
  • Duplicate IDs break aria-labelledby silently -- the spec says to use the first element with a matching ID, but browsers are inconsistent; ensure IDs are unique per document
  • Generic elements used as interactive controls are invisible to AT -- a <div onclick> without a role, tabindex, and keyboard handler is not discoverable by screen readers, switch access, or voice control
  • The accessibility tree is computed lazily in some browsers -- it may not exist until an assistive technology requests it; DevTools forces computation, so testing in DevTools does not guarantee the AT receives the same tree