Screen Reader Testing Without a Screen Reader
Screen reader testing without a screen reader is possible through predictive, heuristic-based analysis of your HTML. Instead of installing VoiceOver on a Mac, NVDA on Windows, or TalkBack on Android, you can run a single command that predicts what each screen reader would announce for your markup. Speakable makes this cross-platform problem solvable from any machine with Node.js installed, giving developers immediate feedback on accessibility without switching operating systems or learning screen reader navigation.
The Cross-Platform Testing Problem
Each major screen reader runs on a specific operating system. VoiceOver requires macOS or iOS. NVDA and JAWS require Windows. TalkBack requires Android. Narrator requires Windows 10 or later. This means comprehensive screen reader testing traditionally requires access to multiple machines or virtual environments.
For most development teams, this creates a bottleneck. Frontend developers on macOS cannot easily test with NVDA. Windows developers cannot test with VoiceOver. The result is that teams test with whichever reader is available locally and hope the others behave similarly. They often do not.
Screen readers differ in real, impactful ways. VoiceOver says "dimmed" where NVDA says "unavailable". JAWS sometimes reads parent container labels. Narrator adds interaction hints that other readers omit. A component that sounds clear in VoiceOver might be confusing in NVDA because of different state phrasing.
Predictive testing addresses this by modeling all four screen readers from a single input. You write HTML once, run Speakable once, and see how all four readers would likely announce it. No OS switching, no VM management, no learning four sets of keyboard shortcuts just to check your button labels.
How Predictive Screen Reader Testing Works
Speakable does not launch a screen reader process, synthesize speech, or connect to platform accessibility APIs. Instead, it uses a three-stage pipeline that transforms HTML into predicted speech output.
Accessibility Tree Extraction
The first stage parses your HTML and builds a canonical accessibility tree. This tree mirrors what the browser would expose through its accessibility API: each element gets a computed role (from native HTML semantics or explicit ARIA), an accessible name (following the W3C accname computation algorithm), states, properties, and parent-child relationships. Elements hidden with aria-hidden="true" or display: none are excluded from the tree, just as a browser would exclude them from the real accessibility API.
Heuristic Renderer Models
The accessibility tree is then passed through four independent renderer models. Each renderer encodes the announcement patterns observed in a specific screen reader across hundreds of test cases. The renderers handle:
- Announcement ordering (name first vs role first, where states appear)
- State vocabulary ("unavailable" vs "dimmed" vs "disabled")
- Role phrasing ("edit" vs "text field" vs "editable text")
- Inclusion or omission of interaction hints
- Grouping behavior (how containers affect child announcements)
- Punctuation handling (which symbols are spoken, which are ignored)
Output Comparison Across Four Readers
The final output shows the predicted announcement for each screen reader side by side. Here is an example for a checkbox component:
npx @reticular/speakable checkbox.html -f text -s all # === NVDA === # Accept terms and conditions, check box, not checked, required # # === JAWS === # Accept terms and conditions, check box, not checked, required # # === VoiceOver === # Accept terms and conditions, required, unchecked, checkbox # # === Narrator === # Accept terms and conditions, check box, not checked, required
Notice how VoiceOver reorders the announcement (putting required before the state) and uses "unchecked" rather than "not checked". These cross-reader differences are exactly what predictive testing reveals without needing access to each platform.
What You Can Validate Without a Screen Reader
Predictive testing covers a substantial portion of what manual testing reveals. These categories of issues are fully detectable from HTML markup alone:
Accessible Names and Descriptions
Every interactive element needs a name that screen readers will announce. Speakable computes the accessible name using the same algorithm browsers use and shows exactly what will be spoken. If a button, link, or input has an empty or missing name, the output will show it immediately.
ARIA State Announcements
States like aria-expanded, aria-checked, aria-selected, and aria-pressed are communicated differently by each screen reader. Predictive testing shows whether these states are being announced at all, and how each reader phrases them.
Heading and Landmark Structure
Screen reader users navigate by headings and landmarks. Speakable's audit mode reports the heading hierarchy (flagging skipped levels) and landmark coverage (whether main, navigation, and banner regions exist). This directly reflects what users experience when pressing H to jump between headings.
Announcement Order Differences
Some screen readers announce the name first, then the role; others announce them in a different order. Some include descriptions inline, others announce them after a pause. Predictive testing shows these ordering differences clearly, helping you write documentation or design decisions that account for all readers.
What Still Requires a Real Screen Reader
Predictive testing has real limitations. Some aspects of the screen reader experience cannot be determined from static HTML analysis:
Timing and Speech Rate
Real screen readers have speech rates, pauses between elements, and timing-dependent behavior (like interrupting speech when content changes). These temporal aspects are not part of static analysis.
Browse Mode vs Focus Mode
NVDA and JAWS have two interaction modes: browse mode (read the page like a document) and focus mode (keystrokes go to the page for interactive widgets). Mode switching behavior depends on runtime focus state and JavaScript event handling, which static analysis cannot replicate.
Screen Reader Settings Variations
Users configure their screen readers with different verbosity levels, punctuation modes, and language settings. Speakable predicts output at default settings. A user running NVDA in "brief" mode will hear less than what Speakable predicts; a user in "most" verbosity will hear more.
Interaction Patterns
Keyboard traps, focus management in modals, live region announcements triggered by JavaScript, and virtual cursor behavior all require a running screen reader to test. These are dynamic behaviors that depend on JavaScript execution and user input sequences that cannot be predicted from HTML alone.
Using Speakable as a Pre-Testing Filter
The most effective use of predictive testing is as a filter that runs before manual testing. Think of it like a linter for screen reader output: it catches the obvious issues instantly so your manual testing time is spent on the things only humans can evaluate.
# Run audit mode to find issues before manual testing npx @reticular/speakable dialog.html -f audit # Sample output: # ISSUE: button with no accessible name at root > dialog > footer > button:nth-child(2) # ISSUE: heading level skipped (h1 to h3) at root > dialog > section # ISSUE: no landmark structure detected # # 3 issues found. Exit code: 1
Fix these issues first. Then open VoiceOver and test the interaction: Does the dialog trap focus correctly? Does the Escape key close it? Is the return focus target correct? These interaction questions require a real screen reader, but the markup questions (names, roles, structure) are already answered.
In CI, this filter prevents regressions from reaching production. A pull request that removes a button label will fail the Speakable audit before a human ever needs to test it.
Recommended Workflow: Predictive First, Manual Second
Here is a practical workflow that maximizes coverage while minimizing the time spent in manual testing:
- Write your component markup. Focus on semantic HTML, proper labeling, and logical structure.
- Run Speakable locally. Check that all four screen readers will announce names, roles, and states correctly. Fix any issues the audit surfaces.
- Commit and push. Speakable runs in CI and catches regressions if anyone later modifies the component.
- Schedule manual testing. Before a release, open VoiceOver (or NVDA) and navigate the component. Focus on interaction flow, timing, and overall comprehension.
- Document findings. If manual testing reveals issues not caught by predictive testing, file them and fix them. These are likely interaction-pattern bugs.
This workflow catches approximately 80% of screen reader issues during step 2 (free, instant, cross-platform) and reserves the remaining 20% for step 4 (manual, thorough, platform-specific).
# Quick local check during development npx @reticular/speakable src/components/Modal.html -f text -s all # CI check on every pull request (in GitHub Actions) # - name: Screen reader regression check # run: npx @reticular/speakable src/components/*.html -f audit # # Fails with exit code 1 if issues found
The key insight is that predictive testing and manual testing are complementary, not competing. Predictive testing catches what is detectable from markup. Manual testing catches what requires interaction. Together, they provide comprehensive coverage without requiring every developer to be a screen reader expert.