How Screen Readers Actually Work: From DOM to Speech
Screen readers don't read pixels. They read semantics. When a blind user navigates your website, they're not perceiving your carefully designed visual layout—they're hearing an interpretation of your markup filtered through an accessibility tree, platform APIs, and text-to-speech synthesis. Understanding this pipeline is essential for any developer writing accessible code.
The process works like this: a browser parses your HTML into a DOM, then builds a parallel accessibility tree containing only semantically relevant information—roles, names, states, and relationships. Operating system accessibility APIs expose this tree to assistive technology. The screen reader queries these APIs and presents information to users through speech or braille.
If your markup is wrong, your product is wrong for screen reader users. There's no workaround. A `<div>` styled to look like a button might be visually indistinguishable from a `<button>`, but screen readers expose it differently. Missing labels, improper headings, and broken ARIA all create barriers that no amount of visual polish can fix. According to WebAIM's 2024 Million report, 54.5% of home pages have images with missing alternative text—a problem that makes those images completely invisible to screen reader users.
Key Takeaways
Understanding the screen reader pipeline helps developers write code that actually works with assistive technology.
- The accessibility tree, not the DOM, is what matters – Browsers create a parallel structure containing only semantic information; screen readers read this, not the raw DOM
- Native HTML provides the best semantics – A `<button>` is inherently accessible; a `<div onclick>` requires extensive work to replicate that accessibility
- Accessible names have a computation order – Content, aria-label, aria-labelledby, and label elements combine according to specific rules developers must understand
- Reading order follows DOM order, not visual order – CSS can reposition elements visually while leaving DOM order unchanged, creating confusion
- Dynamic updates need explicit handling – SPAs must manage focus and use ARIA live regions because screen readers don't automatically detect content changes
The Mental Model
Picture the flow from your code to a screen reader user's ears. Each step transforms the previous one, and problems at any stage create barriers.
The Pipeline
- Developer writes HTML/CSS/JavaScript – This is what you control
- Browser parses HTML into DOM – Document Object Model represents the page structure
- Browser computes styles and layout – CSS is applied, positions calculated
- Browser builds accessibility tree – Semantic information is extracted into a parallel structure
- OS accessibility APIs expose the tree – Windows (UIA/MSAA), macOS (NSAccessibility), Linux (ATK)
- Screen reader queries accessibility APIs – JAWS, NVDA, VoiceOver, TalkBack request information
- Screen reader generates speech/braille – User perceives the final output
Notice that CSS and visual presentation largely drop out after step 3. The accessibility tree doesn't know your button is red or that you used a beautiful font. It knows the button's role, name, and state.
What Enters the Accessibility Tree
Not everything in the DOM appears in the accessibility tree. The browser filters based on semantic relevance:
+--------------------------------------------------+----------------------------------------------------+
| DOM Element | Accessibility Tree |
+--------------------------------------------------+----------------------------------------------------+
| `<button>Submit</button>` | Role: button, Name: "Submit" |
+--------------------------------------------------+----------------------------------------------------+
| `<img src="photo.jpg" alt="Team photo">` | Role: image, Name: "Team photo" |
+--------------------------------------------------+----------------------------------------------------+
| `<div style="display:none">Hidden</div>` | Not included (hidden) |
+--------------------------------------------------+----------------------------------------------------+
| `<span aria-hidden="true">Decorative</span>` | Not included (aria-hidden) |
+--------------------------------------------------+----------------------------------------------------+
| `<div>Some text</div>` | Role: generic, Name: (none) |
+--------------------------------------------------+----------------------------------------------------+
| `<h2>Section Title</h2>` | Role: heading (level 2), Name: "Section Title" |
+--------------------------------------------------+----------------------------------------------------+The accessibility tree is a simplified, semantic view of your content. That's what screen readers work with.
DOM vs. Accessibility Tree
The distinction between DOM and accessibility tree is the single most important concept for accessible development.
The DOM: What You Write
The DOM represents document structure: elements, attributes, and their relationships. It includes everything—semantic elements, generic containers, styles, scripts, everything.
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/products">Products</a></li>
</ul>
</nav>
<main>
<h1>Our Products</h1>
<div class="product-grid">
<article>...</article>
</div>
</main>The Accessibility Tree: What Users Get
The accessibility tree extracts semantic information:
navigation "Main navigation"
list
listitem
link "Home"
listitem
link "Products"
main
heading level 1 "Our Products"
articleNotice what's preserved (navigation landmark, heading level, link names) and what's dropped (the `<ul>`/`<li>` structure becomes a simple list; the `div.product-grid` disappears entirely).
What Doesn't Appear
Several things remove elements from the accessibility tree:
- `display: none` – Element is not rendered, not in tree
- `visibility: hidden` – Element takes space but is hidden from AT
- `aria-hidden="true"` – Explicitly hidden from accessibility APIs
- Elements with no semantic meaning – Empty `<div>`s and `<span>`s may collapse
Understanding these rules is essential for techniques like visually hidden text that should be announced but not seen.
Why Native HTML Beats ARIA
The first rule of ARIA is: don't use ARIA. Native HTML elements come with built-in accessibility that ARIA can only approximate.
Native Elements: Free Accessibility
A native `<button>` element provides:
- Role – Automatically exposed as "button" to screen readers
- Keyboard operation – Responds to Enter and Space automatically
- Focus management – Natively focusable without tabindex
- States – Disabled state exposed when you use the `disabled` attribute
- Activation – Click handlers fire from keyboard without extra code
All of this comes free. The browser handles it.
ARIA Reconstruction: Expensive and Error-Prone
To make a `<div>` behave as a button, you need:
<div
role="button"
tabindex="0"
aria-disabled="false"
onclick="handleClick()"
onkeydown="handleKeyDown(event)"
>
Click me
</div>Plus JavaScript:
function handleKeyDown(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleClick();
}
}And you might still miss edge cases: the disabled state styling, the focus ring, form submission behavior, and so on.
When ARIA Is Necessary
Use ARIA when native HTML can't express the semantics you need:
- Tab panels – No native `<tablist>`, `<tab>`, `<tabpanel>` elements
- Comboboxes – Complex autocomplete patterns need ARIA
- Tree views – Hierarchical navigation requires ARIA roles
- Live regions – Dynamic announcements use `aria-live`
- Custom widgets – Non-standard interaction patterns
Even then, follow the ARIA Authoring Practices to implement patterns correctly.
Accessible Name Computation
Every interactive element needs an accessible name—the text a screen reader announces when the user focuses the element. Browsers compute this name following a specific algorithm.
The Computation Order
The browser checks these sources in order, using the first one that provides a name:
- aria-labelledby – References another element's content by ID
- aria-label – Directly specifies the name
- Native labeling – `<label for="...">`, `alt` attribute, or element content
- Title attribute – Fallback (unreliable; avoid as primary label)
- Placeholder – Only for inputs; disappears when typing; poor accessibility
Understanding this order matters when you have conflicting labels.
Common Name Patterns
+----------------------------------------------------+-----------------------------------+
| Element | How Name Is Derived |
+----------------------------------------------------+-----------------------------------+
| `<button>Submit</button>` | Text content: "Submit" |
+----------------------------------------------------+-----------------------------------+
| `<button aria-label="Close">X</button>` | aria-label: "Close" |
+----------------------------------------------------+-----------------------------------+
| `<img src="..." alt="Company logo">` | alt attribute: "Company logo" |
+----------------------------------------------------+-----------------------------------+
| `<input id="email"><label for="email">Email</label>` | Associated label: "Email" |
+----------------------------------------------------+-----------------------------------+
| `<a href="/pricing">See our plans</a>` | Text content: "See our plans" |
+----------------------------------------------------+-----------------------------------+Common Failures
Icon buttons without names:
<!-- Bad: screen reader says "button" -->
<button><svg>...</svg></button>
<!-- Good: screen reader says "Close menu" -->
<button aria-label="Close menu"><svg>...</svg></button>Duplicate IDs breaking aria-labelledby:
<!-- Bad: which "title" is referenced? -->
<h2 id="title">First</h2>
<h2 id="title">Second</h2>
<div aria-labelledby="title">...</div>Labels that exist visually but aren't associated:
<!-- Bad: label isn't programmatically associated -->
<span>Email:</span>
<input type="email">
<!-- Good: label is associated via for/id -->
<label for="email">Email:</label>
<input type="email" id="email">These failures create elements that screen reader users can reach but can't understand.
Reading Order vs. Visual Order
Screen readers generally follow DOM order. CSS can reposition elements visually while leaving DOM order unchanged. This creates a potential mismatch that confuses users.
The Problem
Consider this layout:
<div class="grid">
<aside class="sidebar">Navigation</aside>
<main class="content">Main content</main>
</div>With CSS Grid:
.grid { display: grid; grid-template-columns: 1fr 3fr; }
.content { grid-column: 1; } /* Visually first */
.sidebar { grid-column: 2; } /* Visually second */Sighted users see main content first, sidebar second. Screen reader users hear sidebar first, main content second—because DOM order hasn't changed.
Best Practices
- Keep DOM order aligned with visual order for primary flows
- Use semantic landmarks (main, nav, aside) so users can navigate directly to sections
- Don't use CSS order extensively for important content sequence
- Test with screen readers to verify reading order makes sense
Flexbox's `order` property and Grid's placement have the same issue. The DOM is what screen readers traverse.
When Order Differences Are Acceptable
Some cases are fine:
- Visually hidden content that's read but not seen
- Mobile-first responsive changes where DOM order matches mobile (primary experience)
- Decorative reordering that doesn't affect comprehension
The key is testing. If a screen reader user can follow the content logically, the order is acceptable.
Navigation Modes
Screen reader users navigate differently than sighted users. Understanding these modes helps developers structure content for efficient navigation.
Browse Mode (Virtual Cursor)
In browse mode, users navigate through content using various methods:
- Arrow keys – Move through content character by character or line by line
- Heading navigation – Press H to jump to next heading, Shift+H for previous
- Landmark navigation – Jump directly to `<main>`, `<nav>`, `<footer>`
- Link list – Pull up a list of all links on the page
- Form control navigation – Press F to jump between form fields
This is why heading structure matters so much—it's how users scan pages.
Focus Mode (Forms Mode)
When users enter an interactive element (input field, custom widget), the screen reader often switches to focus mode:
- Standard keyboard goes to the element, not screen reader navigation
- Arrow keys might control the widget (like selecting from a dropdown)
- Tab moves to next focusable element
Screen readers switch modes automatically when entering form controls. Custom widgets may need `role="application"` or specific ARIA to trigger appropriate mode switching.
How This Affects Development
- Use real headings (`<h1>`-`<h6>`) for page structure—users navigate by them
- Include landmarks (`<main>`, `<nav>`, `<header>`, `<footer>`, `<aside>`)—users jump directly to them
- Make link text descriptive—users often scan link lists out of context
- Follow keyboard patterns for custom widgets—users expect standard behaviors
Dynamic Content and Focus Management
Single-page applications and dynamic interfaces introduce challenges screen readers don't automatically handle.
The Problem with SPAs
Traditional page navigation:
- User clicks link
- Browser loads new page
- Screen reader announces page title
- Focus resets to top of page
SPA navigation:
- User clicks link
- JavaScript updates content
- URL changes via history API
- Screen reader... doesn't know anything changed
Without explicit handling, screen reader users are stuck on the old content, unaware that the page has updated.
Focus Management Solutions
Move focus after navigation:
// After SPA route change
const mainContent = document.querySelector('main');
mainContent.setAttribute('tabindex', '-1');
mainContent.focus();Announce route changes:
<div aria-live="polite" class="visually-hidden" id="route-announcer"></div>// After route change
document.getElementById('route-announcer')
.textContent = 'Navigated to Products page';ARIA Live Regions
For dynamic content updates that don't involve focus:
<div aria-live="polite" aria-atomic="true">
<!-- Content changes will be announced -->
Cart: 3 items
</div>Live region options:
+---------------------+--------------------------------------------+-------------------------------------------------+
| Attribute | Values | Behavior |
+---------------------+--------------------------------------------+-------------------------------------------------+
| `aria-live` | `polite`, `assertive`, `off` | When to interrupt |
+---------------------+--------------------------------------------+-------------------------------------------------+
| `aria-atomic` | `true`, `false` | Announce entire region or just changed part |
+---------------------+--------------------------------------------+-------------------------------------------------+
| `aria-relevant` | `additions`, `removals`, `text`, `all` | What changes to announce |
+---------------------+--------------------------------------------+-------------------------------------------------+Use `polite` for most updates. Reserve `assertive` for critical alerts.
Modal Focus Trapping
When opening a modal:
- Move focus into the modal (first focusable element or heading)
- Trap focus (Tab cycles within modal; doesn't escape to page behind)
- Close on Escape
- Return focus to the triggering element when closing
This pattern is documented in ARIA Authoring Practices for dialogs.
Debugging Accessibility
Developers can inspect the accessibility tree to understand what screen readers will perceive.
Browser DevTools
Chrome DevTools:
- Open DevTools (F12)
- Elements panel → Accessibility pane
- Select an element to see its accessible name, role, and properties
Firefox Accessibility Inspector:
- Open DevTools
- Accessibility tab
- Browse the full accessibility tree
These tools show exactly what the browser exposes to assistive technology.
Practical Debugging Workflow
- Inspect the accessibility tree – Does the element have the expected role and name?
- Check keyboard access – Can you Tab to it? Does Enter/Space activate it?
- Test with a screen reader – Quick test with NVDA (Windows) or VoiceOver (Mac)
- Run automated checks – axe-core catches common WCAG violations
For the 54.5% of pages with missing alt text (WebAIM data), DevTools will show images with empty accessible names—immediately flagging the problem.
Common Issues to Check
+---------------------+------------------------+---------------------------------------------------+
| What to Check | Where to Look | What You're Looking For |
+---------------------+------------------------+---------------------------------------------------+
| Accessible name | Accessibility pane | Name isn't empty; name makes sense |
+---------------------+------------------------+---------------------------------------------------+
| Role | Accessibility pane | Matches element purpose (button, link, etc.) |
+---------------------+------------------------+---------------------------------------------------+
| Focus order | Tab through page | Logical, efficient order |
+---------------------+------------------------+---------------------------------------------------+
| States | Accessibility pane | Expanded/collapsed, checked, disabled exposed |
+---------------------+------------------------+---------------------------------------------------+
| Headings | Accessibility tree | Proper hierarchy, no skipped levels |
+---------------------+------------------------+---------------------------------------------------+
| Landmarks | Accessibility tree | Main, nav, header, footer present |
+---------------------+------------------------+---------------------------------------------------+FAQ
Do screen readers actually read the DOM directly?
No. Screen readers read the accessibility tree, which browsers construct from the DOM. The accessibility tree filters out non-semantic information (decorative elements, hidden content) and exposes semantic information (roles, names, states). The DOM is the input; the accessibility tree is the output that screen readers consume.
Why does my button work visually but fail for screen readers?
If you used a `<div>` or `<span>` instead of `<button>`, the accessibility tree exposes it without the button role. You can add `role="button"`, but you also need `tabindex="0"` for focusability and keyboard event handlers for Enter/Space activation. Using native `<button>` solves this automatically. Check the DevTools accessibility pane to see what role is being exposed.
How do I test my site with a screen reader?
Start with VoiceOver on Mac (Cmd+F5 to toggle) or NVDA on Windows (free download). Basic testing: navigate your site using Tab and arrow keys, listening to what's announced. Check that interactive elements are announced with correct names and roles, headings provide structure, and forms can be completed. See Screen Reader Testing Guide for comprehensive methodology.
Does CSS affect what screen readers announce?
Mostly no, but with exceptions. `display: none` and `visibility: hidden` remove elements from the accessibility tree. `aria-hidden="true"` hides content from AT while keeping it visible. CSS can reorder elements visually without changing DOM order, creating reading order confusion. Beyond that, colors, fonts, and positioning don't affect screen reader output—only semantic structure matters.
Why do screen readers sometimes read things in the wrong order?
Screen readers follow DOM order, not visual order. If CSS Grid or Flexbox reposition elements visually, the announced order may differ from what sighted users see. Check your DOM structure and ensure it matches the logical reading sequence. If elements must be visually reordered, test with a screen reader to ensure the experience still makes sense.
Can ARIA fix any accessibility problem?
No. ARIA can add or modify semantics—giving a `<div>` a button role, adding names, indicating states. But ARIA cannot make an element keyboard focusable (you need `tabindex`), cannot add keyboard event handling (you need JavaScript), and cannot fix structural issues like heading hierarchy. ARIA patches semantics; it doesn't replace proper HTML and interaction code.
Related Resources
Internal Links
- What Is Digital Accessibility? A Technical Definition
- Why Accessibility Is a Software Engineering Problem
- Screen Reader Testing Guide: A Developer's Complete Methodology
- Keyboard Navigation Testing: What Every Developer Needs to Know
- Accessibility Testing Tools: Manual vs Automated vs AI
- How to Make a Website ADA Compliant (Step-by-Step for Developers)
External Sources
- MDN: Accessibility Tree
- W3C ARIA Authoring Practices Guide
- W3C WCAG 2.2 Official Specification
- WebAIM Million 2024 Report
- W3C Accessible Name Computation
- NVDA User Guide
This article was written by TestParty's editorial team with AI assistance. All statistics and claims have been verified against primary sources. Last updated: January 2026.
Stay informed
Accessibility insights delivered
straight to your inbox.


Automate the software work for accessibility compliance, end-to-end.
Empowering businesses with seamless digital accessibility solutions—simple, inclusive, effective.
Book a Demo