Skip to main content
Skip to docs content

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:

  1. Write your component markup. Focus on semantic HTML, proper labeling, and logical structure.
  2. Run Speakable locally. Check that all four screen readers will announce names, roles, and states correctly. Fix any issues the audit surfaces.
  3. Commit and push. Speakable runs in CI and catches regressions if anyone later modifies the component.
  4. Schedule manual testing. Before a release, open VoiceOver (or NVDA) and navigate the component. Focus on interaction flow, timing, and overall comprehension.
  5. 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.

Related Pages