Why screen readers never see the DOM, but a tree of their own
Screen readers and other assistive technologies never read HTML directly, they consume a parallel structure computed by the browser called the accessibility tree. Understanding how DOM, semantic HTML, and ARIA attributes shape this tree lets you track down and fix missing or wrong accessible names with the right tools, instead of testing accessibility by guesswork.
Table of Contents
- 1. What the accessibility tree is and why it matters
- 2. DOM tree and accessibility tree: two parallel structures
- 3. How browsers compute the accessibility tree
- 4. Semantic HTML as the tree's foundation
- 5. ARIA attributes and their effect on the tree
- 6. Accessible name computation in detail
- 7. Debugging tools: DevTools, VoiceOver, NVDA, and axe-core
- 8. Common mistakes: missing or wrong accessible names
- 9. Accessibility tree patterns compared
- 10. Summary
- 11. FAQ
1. What the accessibility tree is and why it matters
The accessibility tree is the structure that screen readers, braille displays, and speech input software actually consume, not the HTML and not the DOM tree. The browser derives a second tree from the DOM, alongside the render tree, where every relevant node carries a role, a computed name, an optional description, and states such as "selected" or "disabled". This structure is passed to assistive technology through platform-specific APIs such as UI Automation on Windows, AT-SPI on Linux, or NSAccessibility on macOS.
This distinction is not an academic subtlety, it is the root cause of many accessibility bugs. An element can look visually perfect and still arrive in the accessibility tree as a nameless node with no role, for instance a clickable div without semantic markup. For sighted users the button is there, for screen reader users it effectively does not exist. Anyone serious about testing accessibility needs to inspect the tree, not just the visual rendering in the browser.
2. DOM tree and accessibility tree: two parallel structures
The DOM tree fundamentally contains every node of the document, regardless of whether it is visible, hidden, or purely decorative. The accessibility tree is a filtered and semantically enriched derivation of that: purely decorative elements are removed, text nodes are not kept as separate tree nodes but flow into their parent node as a computed name, and elements with aria-hidden="true", display: none, or visibility: hidden do not appear in the tree at all, even though they still exist in the DOM.
This reduction is intentional: screen reader users should not have to navigate through every decorative icon or divider. Problems arise when developers unintentionally strip content out of the tree, for example marking up an informative image with an empty alt="" even though it carries content. The following example shows how the DOM and the accessibility tree diverge for the same markup.
<!-- The DOM keeps every node - the accessibility tree does not -->
<button class="icon-button">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24">
<path d="M10 2a8 8 0 105.3 14L20 20l1.4-1.4-4.7-4.7A8 8 0 0010 2z"/>
</svg>
<span class="sr-only">Search products</span>
</button>
<!-- Purely decorative: pruned entirely from the accessibility tree -->
<img src="divider.png" alt="" role="presentation">
<!-- display:none and visibility:hidden are also pruned from the tree -->
<div class="tooltip" style="display:none;">Only 3 left in stock</div>
<!-- Resulting accessibility tree node for the button above:
role: "button"
name: "Search products"
(the <svg> subtree does not appear as a child node at all) -->
3. How browsers compute the accessibility tree
The browser first parses HTML into the DOM, applies CSS to produce the render tree, and derives the accessibility tree in parallel from DOM, CSS, and ARIA attributes. This is based on the mapping specifications HTML-AAM and ARIA, which define an implicit role for every native HTML element, for instance role="button" for <button> or role="navigation" for <nav>. The role attribute can override that implicit role, as long as the ARIA specification permits the combination.
The tree is not a one-time snapshot, it is recomputed on every relevant DOM, attribute, or visibility change, similar to how the render tree responds to style changes. This matters particularly for Hyvä stores built with Alpine.js: switching from x-show to x-if changes not only visibility but also whether the node exists in the accessibility tree at all. Dynamic states such as an opening mini-cart dropdown must consistently be reflected through aria-expanded and actual DOM presence rather than CSS classes alone, so the tree mirrors the real state.
4. Semantic HTML as the tree's foundation
Native HTML elements come with role, focusability, and keyboard behavior for free. A <button> is automatically a focusable node with role="button" in the accessibility tree and can be activated with Enter or Space, without a single line of ARIA. A <div> with an onclick handler has none of that: no focus, no keyboard operation, no role beyond the generic default. The first rule of the ARIA Authoring Practices Guide effectively says: no ARIA role is better than one bolted on incorrectly.
Landmark elements such as <nav>, <main>, <header>, and <footer> additionally form the navigable skeleton screen reader users rely on to jump directly to content regions rather than listening to the page linearly. These elements carry implicit ARIA landmark roles and save you the explicit markup of role="navigation" and similar.
<!-- WRONG: div soup - no node with interactive semantics in the tree -->
<div class="add-to-cart" onclick="addToCart(sku)">
Add to cart
</div>
<!-- Accessibility tree node: role "generic", not focusable,
no keyboard activation, no pressed/disabled state exposed -->
<!-- RIGHT: native <button> - full semantics for free -->
<button type="button" class="add-to-cart" onclick="addToCart(sku)">
Add to cart
</button>
<!-- Accessibility tree node: role "button", name "Add to cart",
focusable, Enter/Space trigger it, no ARIA required -->
<!-- Landmarks build the skeleton assistive technology users jump between -->
<header>...</header>
<nav aria-label="Main navigation">...</nav>
<main>
<h1>Product catalog</h1>
</main>
<footer>...</footer>
5. ARIA attributes and their effect on the tree
ARIA never changes focus order, keyboard behavior, or visual layout, it only ever changes the nodes of the accessibility tree. The role attribute overrides a node's role, aria-label and aria-labelledby set the computed name, aria-describedby sets the description, and state attributes such as aria-expanded, aria-checked, or aria-selected are exposed as properties on the corresponding tree node, which assistive technology announces immediately. aria-hidden="true" removes the entire subtree beneath an element from the accessibility tree, regardless of its visual visibility.
This is exactly where a common trap lies: aria-hidden="true" on a container that holds a focusable child element removes the announcement but not the tab stop. Keyboard users then land on an element the screen reader ignores completely, a so-called ghost focus. role="presentation" or role="none" only removes the semantics of a single element while leaving its children in the tree, an important difference from aria-hidden.
/* Visually hidden but still present in the accessibility tree - this is
the accessible name source for screen reader users */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* WRONG assumption: CSS generated content is not a reliable accessible
name source. Support is inconsistent across browsers and screen readers. */
.status-badge::before {
content: "New";
}
/* RIGHT: put the text in the DOM as a real node, hide it visually if needed */
.status-badge .sr-only {
/* "New" as a real text node, always exposed to the accessibility tree */
}
6. Accessible name computation in detail
The so-called Accessible Name and Description Computation is a defined algorithm in the ARIA specification and enforces a strict priority order: aria-labelledby counts first, then aria-label, then native labelling mechanisms such as an associated <label> element, alt on images, or caption on tables, and only last the element's visible text content. This order is frequently misunderstood in practice, for instance when a developer adds an aria-label that differs from the visible button text, even though the visible text alone would have been sufficient.
That is exactly what violates WCAG success criterion 2.5.3 Label in Name: users of speech input software such as Dragon NaturallySpeaking say the visibly displayed text to activate an element. If the computed accessible name diverges from the visible text, the voice command fails. Automated tests can reproduce this computation and add it as a regression test to the CI pipeline, instead of relying on manual spot checks with every deployment.
// Unit test: assert the computed accessible name matches intent
// npm install dom-accessibility-api
import { computeAccessibleName } from 'dom-accessibility-api';
const button = document.querySelector('.icon-button');
const name = computeAccessibleName(button);
if (name.trim() === '') {
throw new Error('Button has no accessible name - screen readers announce "button" only');
}
console.assert(name === 'Search products', `Unexpected accessible name: "${name}"`);
// CDP: dump the full accessibility tree for automated regression checks
// (Puppeteer)
const client = await page.target().createCDPSession();
await client.send('Accessibility.enable');
const { nodes } = await client.send('Accessibility.getFullAXTree');
const buttonNode = nodes.find(n => n.name?.value === 'Search products');
7. Debugging tools: DevTools, VoiceOver, NVDA, and axe-core
Chrome DevTools shows the computed properties of a selected node under the Accessibility tab in the Elements panel: name, role, description, and the source the name was computed from. The "Show accessibility tree" button next to the DOM tree renders the complete tree for the current page and immediately reveals which elements are missing or misnamed. Firefox offers a comparable Accessibility Inspector including contrast checks, and macOS ships an equivalent Accessibility Inspector as part of Xcode.
The tree alone does not replace an actual listening test: VoiceOver on macOS (Cmd+F5) and NVDA on Windows, free to use, surface AT-specific quirks that pure tree inspection does not, such as different announcement ordering for nested live regions. For automated, continuous testing, integrate axe-core into the CI pipeline, which returns structured violation reports including the affected HTML snippet and a fix suggestion.
{
"id": "button-name",
"impact": "critical",
"description": "Ensures buttons have discernible text",
"help": "Buttons must have discernible text",
"nodes": [
{
"html": "<button class=\"icon-button\"><svg aria-hidden=\"true\"></svg></button>",
"target": [".icon-button"],
"failureSummary": "Fix any of the following: Element does not have inner text that is visible to screen readers; aria-label attribute does not exist or is empty; aria-labelledby attribute does not exist, references elements that do not exist or are empty; Element has no title attribute"
}
]
}
8. Common mistakes: missing or wrong accessible names
The most frequent mistake is the icon button with no text at all: a <button> whose only content is an <svg> lands in the accessibility tree with a correct role but an empty name, so screen readers announce nothing but "button" with no further information. Just as common: informative images with an empty alt="", which is really meant only for purely decorative images, strip the image content entirely out of the tree even though it visually carries information for sighted users.
A third recurring mistake is an aria-label that does not match the visible text or contradicts it, for example aria-label="Add to cart" on a button whose visible text reads "Buy now". The fourth classic: focusable interactive elements nested inside a container marked aria-hidden="true" create invisible keyboard traps. The fix is the same underlying rule in every case: use visible text as the primary name source, treat aria-label as a last resort, and never set aria-hidden on a container that holds focusable content.
9. Accessibility tree patterns compared
The following patterns show the same class of mistake from different angles: an element exists visually, but the accessibility tree ends up with either no node, the wrong node, or a contradictory one. The right-hand column shows the concrete effect the fix has on the computed tree.
| Problem | Wrong | Right | Effect on the tree |
|---|---|---|---|
| Icon button with no text | <button><svg></svg></button> |
<button aria-label="Search"><svg aria-hidden="true"> |
Empty name becomes a populated name |
| Clickable element | <div onclick="…"> |
<button type="button"> |
role generic becomes role button, focusable |
| Image with content | <img src="chart.png" alt=""> |
<img alt="Revenue up 12% in Q2"> |
Node stays in the tree, name carries content |
| Visible text plus aria-label | <button aria-label="Cart">Buy now</button> |
<button>Buy now</button> |
No Label in Name violation (WCAG 2.5.3) |
| Hidden focus | aria-hidden on a container with a button inside | Remove focusable children or add tabindex="-1" | No ghost tab stop without announcement |
Notably, none of these fixes require piling on extra ARIA attributes. Usually it is enough to use semantically correct HTML and keep the existing name sources consistent, instead of overriding them with an additional aria-label. The accessibility tree does not get more complex as a result, it gets easier to debug.
Mironsoft
Accessibility, accessibility audits, and Hyvä implementation for Magento stores
Want your store's accessibility tree professionally audited?
We analyze the accessibility tree of your Magento and Hyvä templates, find missing or wrong accessible names, and implement semantic and ARIA fixes that are WCAG compliant and testable in automation.
Tree audit
DevTools, VoiceOver, and axe-core analysis of your key templates
Semantic refactoring
Replace div soup with native elements, add ARIA only where needed
CI integration
Integrate axe-core and accessible name tests into your pipeline
10. Summary
The accessibility tree is the real interface between a web page and assistive technology, not the HTML and not the DOM tree. The browser derives it in parallel with the render tree from DOM, semantic HTML, and ARIA attributes, and recomputes it on every relevant change. Native HTML elements provide role, focusability, and keyboard behavior for free, while ARIA selectively adds roles, names, descriptions, and states without affecting focus or layout. Accessible name computation follows a fixed priority from aria-labelledby through aria-label down to visible text content, and ignoring that order leads to empty or contradictory names.
Missing or wrong names are rarely exotic edge cases, they tend to appear at the same recurring spots: icon buttons with no text, informative images with empty alt, contradictory aria-label values, and focusable elements inside aria-hidden containers. Chrome DevTools, the Firefox Accessibility Inspector, real screen reader tests with VoiceOver or NVDA, and automated testing with axe-core in the CI pipeline reliably catch these mistakes before they lock users out in production.
Understanding and debugging the accessibility tree, the essentials at a glance
DOM vs. tree
Screen readers do not read the DOM, they read the browser-computed accessibility tree with role, name, and states.
Accessible name order
aria-labelledby before aria-label before native labelling before visible text. Order determines the final name.
Semantics first
Native elements like <button> and <nav> provide role, focus, and keyboard control without any ARIA.
Debugging tools
DevTools Accessibility pane, VoiceOver, NVDA, and axe-core in the CI pipeline for automated regression tests.