Screen Reader Testing
Practical guide to testing web applications with screen readers for comprehensive accessibility validation.
When to Use This Skill Validating screen reader compatibility Testing ARIA implementations Debugging assistive technology issues Verifying form accessibility Testing dynamic content announcements Ensuring navigation accessibility Core Concepts 1. Major Screen Readers Screen Reader Platform Browser Usage VoiceOver macOS/iOS Safari ~15% NVDA Windows Firefox/Chrome ~31% JAWS Windows Chrome/IE ~40% TalkBack Android Chrome ~10% Narrator Windows Edge ~4% 2. Testing Priority Minimum Coverage: 1. NVDA + Firefox (Windows) 2. VoiceOver + Safari (macOS) 3. VoiceOver + Safari (iOS)
Comprehensive Coverage: + JAWS + Chrome (Windows) + TalkBack + Chrome (Android) + Narrator + Edge (Windows)
- Screen Reader Modes Mode Purpose When Used Browse/Virtual Read content Default reading Focus/Forms Interact with controls Filling forms Application Custom widgets ARIA applications VoiceOver (macOS) Setup Enable: System Preferences → Accessibility → VoiceOver Toggle: Cmd + F5 Quick Toggle: Triple-press Touch ID
Essential Commands Navigation: VO = Ctrl + Option (VoiceOver modifier)
VO + Right Arrow Next element VO + Left Arrow Previous element VO + Shift + Down Enter group VO + Shift + Up Exit group
Reading: VO + A Read all from cursor Ctrl Stop speaking VO + B Read current paragraph
Interaction: VO + Space Activate element VO + Shift + M Open menu Tab Next focusable element Shift + Tab Previous focusable element
Rotor (VO + U): Navigate by: Headings, Links, Forms, Landmarks Left/Right Arrow Change rotor category Up/Down Arrow Navigate within category Enter Go to item
Web Specific: VO + Cmd + H Next heading VO + Cmd + J Next form control VO + Cmd + L Next link VO + Cmd + T Next table
Testing Checklist
VoiceOver Testing Checklist
Page Load
- [ ] Page title announced
- [ ] Main landmark found
- [ ] Skip link works
Navigation
- [ ] All headings discoverable via rotor
- [ ] Heading levels logical (H1 → H2 → H3)
- [ ] Landmarks properly labeled
- [ ] Skip links functional
Links & Buttons
- [ ] Link purpose clear
- [ ] Button actions described
- [ ] New window/tab announced
Forms
- [ ] All labels read with inputs
- [ ] Required fields announced
- [ ] Error messages read
- [ ] Instructions available
- [ ] Focus moves to errors
Dynamic Content
- [ ] Alerts announced immediately
- [ ] Loading states communicated
- [ ] Content updates announced
- [ ] Modals trap focus correctly
Tables
- [ ] Headers associated with cells
- [ ] Table navigation works
- [ ] Complex tables have captions
Common Issues & Fixes
Invalid email
Invalid email
NVDA (Windows) Setup Download: nvaccess.org Start: Ctrl + Alt + N Stop: Insert + Q
Essential Commands Navigation: Insert = NVDA modifier
Down Arrow Next line Up Arrow Previous line Tab Next focusable Shift + Tab Previous focusable
Reading: NVDA + Down Arrow Say all Ctrl Stop speech NVDA + Up Arrow Current line
Headings: H Next heading Shift + H Previous heading 1-6 Heading level 1-6
Forms: F Next form field B Next button E Next edit field X Next checkbox C Next combo box
Links: K Next link U Next unvisited link V Next visited link
Landmarks: D Next landmark Shift + D Previous landmark
Tables: T Next table Ctrl + Alt + Arrows Navigate cells
Elements List (NVDA + F7): Shows all links, headings, form fields, landmarks
Browse vs Focus Mode NVDA automatically switches modes: - Browse Mode: Arrow keys navigate content - Focus Mode: Arrow keys control interactive elements
Manual switch: NVDA + Space
Watch for: - "Browse mode" announcement when navigating - "Focus mode" when entering form fields - Application role forces forms mode
Testing Script
NVDA Test Script
Initial Load
- Navigate to page
- Let page finish loading
- Press Insert + Down to read all
- Note: Page title, main content identified?
Landmark Navigation
- Press D repeatedly
- Check: All main areas reachable?
- Check: Landmarks properly labeled?
Heading Navigation
- Press Insert + F7 → Headings
- Check: Logical heading structure?
- Press H to navigate headings
- Check: All sections discoverable?
Form Testing
- Press F to find first form field
- Check: Label read?
- Fill in invalid data
- Submit form
- Check: Errors announced?
- Check: Focus moved to error?
Interactive Elements
- Tab through all interactive elements
- Check: Each announces role and state
- Activate buttons with Enter/Space
- Check: Result announced?
Dynamic Content
- Trigger content update
- Check: Change announced?
- Open modal
- Check: Focus trapped?
- Close modal
- Check: Focus returns?
JAWS (Windows) Essential Commands Start: Desktop shortcut or Ctrl + Alt + J Virtual Cursor: Auto-enabled in browsers
Navigation: Arrow keys Navigate content Tab Next focusable Insert + Down Read all Ctrl Stop speech
Quick Keys: H Next heading T Next table F Next form field B Next button G Next graphic L Next list ; Next landmark
Forms Mode: Enter Enter forms mode Numpad + Exit forms mode F5 List form fields
Lists: Insert + F7 Link list Insert + F6 Heading list Insert + F5 Form field list
Tables: Ctrl + Alt + Arrows Table navigation
TalkBack (Android) Setup Enable: Settings → Accessibility → TalkBack Toggle: Hold both volume buttons 3 seconds
Gestures Explore: Drag finger across screen Next: Swipe right Previous: Swipe left Activate: Double tap Scroll: Two finger swipe
Reading Controls (swipe up then right): - Headings - Links - Controls - Characters - Words - Lines - Paragraphs
Common Test Scenarios 1. Modal Dialog
Confirm Delete
This action cannot be undone.
// Focus management function openModal(modal) { // Store last focused element lastFocus = document.activeElement;
// Move focus to modal modal.querySelector("h2").focus();
// Trap focus modal.addEventListener("keydown", trapFocus); }
function closeModal(modal) { // Return focus lastFocus.focus(); }
function trapFocus(e) { if (e.key === "Tab") { const focusable = modal.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', ); const first = focusable[0]; const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
last.focus();
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === last) {
first.focus();
e.preventDefault();
}
}
if (e.key === "Escape") { closeModal(modal); } }
- Live Regions
- Tab Interface
// Tab keyboard navigation tablist.addEventListener("keydown", (e) => { const tabs = [...tablist.querySelectorAll('[role="tab"]')]; const index = tabs.indexOf(document.activeElement);
let newIndex; switch (e.key) { case "ArrowRight": newIndex = (index + 1) % tabs.length; break; case "ArrowLeft": newIndex = (index - 1 + tabs.length) % tabs.length; break; case "Home": newIndex = 0; break; case "End": newIndex = tabs.length - 1; break; default: return; }
tabs[newIndex].focus(); activateTab(tabs[newIndex]); e.preventDefault(); });
Debugging Tips // Log what screen reader sees function logAccessibleName(element) { const computed = window.getComputedStyle(element); console.log({ role: element.getAttribute("role") || element.tagName, name: element.getAttribute("aria-label") || element.getAttribute("aria-labelledby") || element.textContent, state: { expanded: element.getAttribute("aria-expanded"), selected: element.getAttribute("aria-selected"), checked: element.getAttribute("aria-checked"), disabled: element.disabled, }, visible: computed.display !== "none" && computed.visibility !== "hidden", }); }
Best Practices Do's Test with actual screen readers - Not just simulators Use semantic HTML first - ARIA is supplemental Test in browse and focus modes - Different experiences Verify focus management - Especially for SPAs Test keyboard only first - Foundation for SR testing Don'ts Don't assume one SR is enough - Test multiple Don't ignore mobile - Growing user base Don't test only happy path - Test error states Don't skip dynamic content - Most common issues Don't rely on visual testing - Different experience Resources VoiceOver User Guide NVDA User Guide JAWS Documentation WebAIM Screen Reader Survey