Accessibility Tree vs DOM: What They Are, How They Differ, and When Each Matters
Accessibility Tree vs DOM
Every web page has two parallel representations in the browser. The DOM (Document Object Model) holds the full HTML structure. The accessibility tree holds only the semantically meaningful parts: buttons, headings, links, form fields, and their relationships. Understanding the difference between these two trees is essential for web developers, automation engineers, and anyone building tools that interact with UI programmatically.
What Is the DOM?
The DOM is the browser's in-memory representation of an HTML document. When the browser parses HTML, it builds a tree of nodes where every element, attribute, and text node gets its own entry. The DOM is what JavaScript interacts with when you call document.querySelector() or element.appendChild().
A simple form in HTML:
<div class="form-wrapper" id="signup">
<div class="field-group">
<label for="email">Email address</label>
<input type="email" id="email" name="email" placeholder="you@example.com" />
</div>
<div class="field-group">
<label for="pass">Password</label>
<input type="password" id="pass" name="pass" />
</div>
<div class="btn-wrapper">
<button type="submit" class="btn btn-primary btn-lg">Create account</button>
</div>
</div>
The DOM tree for this fragment has 13 nodes (the outer div, three inner divs, two labels, two inputs, a button, plus text nodes). Every wrapper div, every CSS class, every id attribute is preserved. The DOM is complete but noisy; most of these nodes exist for layout and styling rather than user interaction.
What Is the Accessibility Tree?
The accessibility tree is a parallel structure the browser builds from the DOM. It strips away layout containers and presentational markup, keeping only elements that have semantic meaning for users. Screen readers, voice control software, and browser automation tools read this tree instead of the raw DOM.
For the same form above, the accessibility tree contains roughly this:
form "signup"
├── label "Email address"
│ └── textbox "Email address" (placeholder: "you@example.com")
├── label "Password"
│ └── textbox "Password" (password)
└── button "Create account"
Five meaningful nodes instead of thirteen. The wrapper divs are gone. The CSS classes are gone. What remains is the semantic structure: two labeled text fields and a button. Each node carries a role (textbox, button), a name (the accessible label), and a state (focused, disabled, expanded).
How the Browser Builds the Accessibility Tree
The browser does not build the accessibility tree from scratch. It derives it from the DOM using a set of rules:
-
Native HTML semantics: A
<button>automatically gets role "button". An<input type="checkbox">gets role "checkbox". The browser knows what these elements mean without any extra attributes. -
ARIA attributes: When native HTML is not enough, developers add ARIA roles, labels, and states.
<div role="tablist">tells the browser this div acts as a tab container.aria-label="Close dialog"provides an accessible name for an icon button with no visible text. -
Label computation: The browser follows a priority chain to determine each element's accessible name. It checks
aria-labelledbyfirst, thenaria-label, then the<label>element association, then the element's text content. -
Pruning: Elements with
aria-hidden="true"orrole="presentation"are removed from the accessibility tree. So are purely decorative elements like empty divs and CSS-generated content that carries no meaning.
Side-by-Side Comparison
| Aspect | DOM | Accessibility Tree | |---|---|---| | What it contains | Every HTML element, attribute, and text node | Only semantically meaningful elements | | Node count (typical page) | 1,500 to 15,000+ | 200 to 2,000 | | Built from | HTML source code | Derived from the DOM + ARIA + browser heuristics | | Primary consumers | JavaScript, CSS engine, DevTools | Screen readers, voice control, automation tools | | Exposes styling info | Yes (classes, inline styles, computed styles) | No | | Exposes element roles | Only if explicitly marked | Yes, always (native or ARIA) | | Changes when CSS changes | Yes (class names, display rules) | Usually not (semantic meaning stays the same) | | Stability across redesigns | Low (class names and structure change) | Higher (roles and labels tend to persist) |
Inspecting Each Tree in Your Browser
Both trees are visible in browser DevTools, but they live in different panels.
DOM: Elements Panel
Open DevTools (F12 or Cmd+Option+I), go to the Elements tab. This shows the DOM tree. You can expand nodes, see attributes, edit content live, and watch the DOM update in real time as JavaScript runs.
Accessibility Tree: Accessibility Panel
In Chrome, open DevTools, go to Elements, then find the "Accessibility" pane in the sidebar (you may need to click the >> overflow menu). This shows the computed accessibility properties for the selected element: its role, name, description, and state.
For a full-page view of the accessibility tree, Chrome also has an experimental feature. Open DevTools, go to Settings, find "Experiments," and enable "Full-page accessibility tree." After reloading DevTools, you will see a toggle in the Elements panel to switch between DOM view and accessibility tree view.
# On macOS, you can also inspect the accessibility tree of native apps
# using the Accessibility Inspector in Xcode
open -a "Accessibility Inspector"
In Firefox, the accessibility tree has its own dedicated tab in DevTools (the "Accessibility" tab), which shows the full tree, highlights issues, and supports keyboard navigation.
When the DOM and the Accessibility Tree Disagree
The two trees can diverge in important ways. Understanding these divergences is where most accessibility bugs hide.
Hidden Content
An element with display: none or visibility: hidden is removed from both the DOM layout and the accessibility tree. But aria-hidden="true" removes an element from the accessibility tree while keeping it visible and interactive in the DOM. This can create a situation where sighted users see a button but screen reader users cannot find it.
Custom Components
A <div> with click handlers looks interactive in the DOM but shows up as generic text in the accessibility tree (role: "generic") unless the developer adds role="button" and keyboard handling. The DOM says "this is a clickable div"; the accessibility tree says "this is just text."
ARIA Overrides
ARIA attributes can completely change how an element appears in the accessibility tree without touching the DOM. Adding role="navigation" to a <div> makes the accessibility tree treat it as a navigation landmark, but the DOM still shows a plain div.
Warning
Using ARIA incorrectly is worse than not using it at all. A button with role="link" confuses screen readers more than an unlabeled button would. The first rule of ARIA: if you can use a native HTML element with the correct semantics, use it instead of adding ARIA to a generic element.
Practical Uses of Each Tree
When to Work with the DOM
- Web development: building pages, manipulating elements with JavaScript, attaching event handlers
- Web scraping: extracting structured data from pages where the HTML structure is the data (tables, lists, article bodies)
- CSS styling: selecting elements by class, ID, or attribute for visual presentation
- Performance profiling: counting DOM nodes, measuring reflow costs, identifying layout thrashing
When to Work with the Accessibility Tree
- Accessibility testing: verifying that all interactive elements have proper roles and labels
- Browser automation: writing selectors that survive site redesigns (Playwright's
getByRole()andgetByLabel()methods use the accessibility tree) - AI agent development: feeding a compact, semantic representation of the page to an LLM instead of the full DOM
- Screen reader testing: verifying the experience for users who rely on assistive technology
// Playwright example: using accessibility tree selectors
// These survive redesigns because they target semantics, not structure
await page.getByRole('button', { name: 'Create account' }).click();
await page.getByLabel('Email address').fill('test@example.com');
await page.getByRole('heading', { level: 1 }).textContent();
// Compare with DOM selectors that break on redesign
await page.click('.btn.btn-primary.btn-lg'); // fragile
await page.fill('#email'); // slightly better, but still structural
Node Count: DOM vs Accessibility Tree on Real Sites
We measured the node count difference on several popular sites to give you a sense of the reduction:
| Site | DOM Nodes | Accessibility Tree Nodes | Reduction | |---|---|---|---| | Google Search results page | ~4,200 | ~350 | 92% | | GitHub repository page | ~6,800 | ~900 | 87% | | Medium article | ~3,100 | ~280 | 91% | | Amazon product page | ~12,000 | ~1,800 | 85% | | Simple marketing landing page | ~800 | ~120 | 85% |
The accessibility tree is typically 85% to 95% smaller than the DOM. For AI agents that need to fit page context into a token window, this compression matters. For accessibility testing, it means you are working with a focused set of elements that actually matter to users.
Common Pitfalls
-
Assuming the accessibility tree is always correct: The tree is only as good as the HTML and ARIA markup. If developers use
<div>for everything and skip ARIA, the accessibility tree will be sparse and misleading. Garbage in, garbage out. -
Relying on DOM selectors for automation: CSS selectors like
.nav-item > a:nth-child(3)work today and break tomorrow. Accessibility tree selectors (getByRole,getByLabel) are more stable because they target what elements mean, not where they sit in the tree. -
Forgetting that the accessibility tree is dynamic: It updates in real time as the DOM changes. Adding an
aria-expanded="true"attribute changes the accessibility tree immediately. If your automation tool caches a snapshot of the tree, it can go stale. -
Over-using aria-hidden: Hiding elements from the accessibility tree should be deliberate and rare. A common mistake is hiding content to "clean up" the screen reader experience and accidentally making important controls unreachable.
Quick Reference Checklist
Here is a checklist for working with both trees effectively:
<button>, <input>, <nav>) before reaching for ARIAgetByRole() and getByLabel() for automation selectorsaria-hidden="true" on interactive elements that users need to reachWrapping Up
The DOM is the complete blueprint of a page. The accessibility tree is its semantic summary. The DOM answers "what HTML is on this page?" while the accessibility tree answers "what can a user actually do here?" For accessibility testing, browser automation, and AI agents that interact with web pages, the accessibility tree provides a cleaner, more stable, and more meaningful representation than the raw DOM. Build with semantic HTML first, test with the accessibility tree, and reach for ARIA only when native elements fall short.
Fazm is an open source macOS AI agent that uses the accessibility tree to understand and control applications on your Mac. Open source on GitHub.