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)3. 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 → VoiceOverToggle: Cmd + F5Quick Toggle: Triple-press Touch IDEssential Commands
Navigation:VO = Ctrl + Option (VoiceOver modifier)
VO + Right Arrow Next elementVO + Left Arrow Previous elementVO + Shift + Down Enter groupVO + Shift + Up Exit group
Reading:VO + A Read all from cursorCtrl Stop speakingVO + B Read current paragraph
Interaction:VO + Space Activate elementVO + Shift + M Open menuTab Next focusable elementShift + Tab Previous focusable element
Rotor (VO + U):Navigate by: Headings, Links, Forms, LandmarksLeft/Right Arrow Change rotor categoryUp/Down Arrow Navigate within categoryEnter Go to item
Web Specific:VO + Cmd + H Next headingVO + Cmd + J Next form controlVO + Cmd + L Next linkVO + Cmd + T Next tableTesting Checklist
markdown
## 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 captionsCommon Issues & Fixes
html
<!-- Issue: Button not announcing purpose --><button><svg>...</svg></button>
<!-- Fix --><button aria-label="Close dialog"><svg aria-hidden="true">...</svg></button>
<!-- Issue: Dynamic content not announced --><div id="results">New results loaded</div>
<!-- Fix --><div id="results" role="status" aria-live="polite">New results loaded</div>
<!-- Issue: Form error not read --><input type="email" /><span class="error">Invalid email</span>
<!-- Fix --><input type="email" aria-invalid="true" aria-describedby="email-error" /><span id="email-error" role="alert">Invalid email</span>NVDA (Windows)
Setup
Download: nvaccess.orgStart: Ctrl + Alt + NStop: Insert + QEssential Commands
Navigation:Insert = NVDA modifier
Down Arrow Next lineUp Arrow Previous lineTab Next focusableShift + Tab Previous focusable
Reading:NVDA + Down Arrow Say allCtrl Stop speechNVDA + Up Arrow Current line
Headings:H Next headingShift + H Previous heading1-6 Heading level 1-6
Forms:F Next form fieldB Next buttonE Next edit fieldX Next checkboxC Next combo box
Links:K Next linkU Next unvisited linkV Next visited link
Landmarks:D Next landmarkShift + D Previous landmark
Tables:T Next tableCtrl + Alt + Arrows Navigate cells
Elements List (NVDA + F7):Shows all links, headings, form fields, landmarksBrowse 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 modeTesting Script
markdown
## NVDA Test Script
### Initial Load
1. Navigate to page2. Let page finish loading3. Press Insert + Down to read all4. Note: Page title, main content identified?
### Landmark Navigation
1. Press D repeatedly2. Check: All main areas reachable?3. Check: Landmarks properly labeled?
### Heading Navigation
1. Press Insert + F7 → Headings2. Check: Logical heading structure?3. Press H to navigate headings4. Check: All sections discoverable?
### Form Testing
1. Press F to find first form field2. Check: Label read?3. Fill in invalid data4. Submit form5. Check: Errors announced?6. Check: Focus moved to error?
### Interactive Elements
1. Tab through all interactive elements2. Check: Each announces role and state3. Activate buttons with Enter/Space4. Check: Result announced?
### Dynamic Content
1. Trigger content update2. Check: Change announced?3. Open modal4. Check: Focus trapped?5. Close modal6. Check: Focus returns?JAWS (Windows)
Essential Commands
Start: Desktop shortcut or Ctrl + Alt + JVirtual Cursor: Auto-enabled in browsers
Navigation:Arrow keys Navigate contentTab Next focusableInsert + Down Read allCtrl Stop speech
Quick Keys:H Next headingT Next tableF Next form fieldB Next buttonG Next graphicL Next list; Next landmark
Forms Mode:Enter Enter forms modeNumpad + Exit forms modeF5 List form fields
Lists:Insert + F7 Link listInsert + F6 Heading listInsert + F5 Form field list
Tables:Ctrl + Alt + Arrows Table navigationTalkBack (Android)
Setup
Enable: Settings → Accessibility → TalkBackToggle: Hold both volume buttons 3 secondsGestures
Explore: Drag finger across screenNext: Swipe rightPrevious: Swipe leftActivate: Double tapScroll: Two finger swipe
Reading Controls (swipe up then right):- Headings- Links- Controls- Characters- Words- Lines- ParagraphsCommon Test Scenarios
1. Modal Dialog
html
<!-- Accessible modal structure --><div role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describedby="dialog-desc"> <h2 id="dialog-title">Confirm Delete</h2> <p id="dialog-desc">This action cannot be undone.</p> <button>Cancel</button> <button>Delete</button></div>javascript
// Focus managementfunction 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); }}2. Live Regions
html
<!-- Status messages (polite) --><div role="status" aria-live="polite" aria-atomic="true"> <!-- Content updates will be announced after current speech --></div>
<!-- Alerts (assertive) --><div role="alert" aria-live="assertive"> <!-- Content updates interrupt current speech --></div>
<!-- Progress updates --><div role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100" aria-label="Upload progress"></div>
<!-- Log (additions only) --><div role="log" aria-live="polite" aria-relevant="additions"> <!-- New messages announced, removals not --></div>3. Tab Interface
html
<div role="tablist" aria-label="Product information"> <button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1"> Description </button> <button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2" tabindex="-1" > Reviews </button></div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1"> Product description content...</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden> Reviews content...</div>javascript
// Tab keyboard navigationtablist.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
javascript
// Log what screen reader seesfunction 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


