screen-reader-testing by wshobson/agents
npx skills add https://github.com/wshobson/agents --skill screen-reader-testing通过屏幕阅读器测试 Web 应用程序的实用指南,用于全面的无障碍访问验证。
| 屏幕阅读器 | 平台 | 浏览器 | 使用率 |
|---|---|---|---|
| VoiceOver | macOS/iOS | Safari | ~15% |
| NVDA | Windows | Firefox/Chrome | ~31% |
| JAWS | Windows | Chrome/IE | ~40% |
| TalkBack | Android | Chrome |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| ~10% |
| Narrator | Windows | Edge | ~4% |
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)
| 模式 | 目的 | 使用时机 |
|---|---|---|
| 浏览/虚拟光标 | 阅读内容 | 默认阅读模式 |
| 焦点/表单 | 与控件交互 | 填写表单时 |
| 应用 | 自定义组件 | ARIA 应用程序 |
Enable: System Preferences → Accessibility → VoiceOver
Toggle: Cmd + F5
Quick Toggle: Triple-press Touch ID
Navigation:
VO = Ctrl + Option (VoiceOver modifier)
VO + Right Arrow 下一个元素
VO + Left Arrow 上一个元素
VO + Shift + Down 进入组
VO + Shift + Up 退出组
Reading:
VO + A 从光标处开始阅读全部内容
Ctrl 停止朗读
VO + B 阅读当前段落
Interaction:
VO + Space 激活元素
VO + Shift + M 打开菜单
Tab 下一个可聚焦元素
Shift + Tab 上一个可聚焦元素
Rotor (VO + U):
Navigate by: Headings, Links, Forms, Landmarks
Left/Right Arrow 切换转子类别
Up/Down Arrow 在类别内导航
Enter 跳转到项目
Web Specific:
VO + Cmd + H 下一个标题
VO + Cmd + J 下一个表单控件
VO + Cmd + L 下一个链接
VO + Cmd + T 下一个表格
## VoiceOver Testing Checklist
### Page Load
- [ ] 页面标题已播报
- [ ] 找到主要地标
- [ ] 跳过链接有效
### Navigation
- [ ] 所有标题可通过转子发现
- [ ] 标题层级逻辑正确 (H1 → H2 → H3)
- [ ] 地标区域标签正确
- [ ] 跳过链接功能正常
### Links & Buttons
- [ ] 链接目的明确
- [ ] 按钮操作描述清晰
- [ ] 新窗口/标签页已播报
### Forms
- [ ] 所有标签随输入框朗读
- [ ] 必填字段已播报
- [ ] 错误信息已朗读
- [ ] 说明信息可用
- [ ] 焦点移动到错误处
### Dynamic Content
- [ ] 警告信息立即播报
- [ ] 加载状态已传达
- [ ] 内容更新已播报
- [ ] 模态框正确捕获焦点
### Tables
- [ ] 表头与单元格关联
- [ ] 表格导航有效
- [ ] 复杂表格有标题说明
<!-- 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>
Download: nvaccess.org
Start: Ctrl + Alt + N
Stop: Insert + Q
Navigation:
Insert = NVDA modifier
Down Arrow 下一行
Up Arrow 上一行
Tab 下一个可聚焦元素
Shift + Tab 上一个可聚焦元素
Reading:
NVDA + Down Arrow 朗读全部
Ctrl 停止朗读
NVDA + Up Arrow 当前行
Headings:
H 下一个标题
Shift + H 上一个标题
1-6 标题级别 1-6
Forms:
F 下一个表单字段
B 下一个按钮
E 下一个编辑框
X 下一个复选框
C 下一个组合框
Links:
K 下一个链接
U 下一个未访问链接
V 下一个已访问链接
Landmarks:
D 下一个地标
Shift + D 上一个地标
Tables:
T 下一个表格
Ctrl + Alt + Arrows 导航单元格
Elements List (NVDA + F7):
显示所有链接、标题、表单字段、地标
NVDA 自动切换模式:
- 浏览模式:方向键导航内容
- 焦点模式:方向键控制交互元素
手动切换:NVDA + Space
注意监听:
- 导航时播报"浏览模式"
- 进入表单字段时播报"焦点模式"
- 应用角色强制进入表单模式
## NVDA Test Script
### Initial Load
1. 导航到页面
2. 等待页面加载完成
3. 按 Insert + Down 朗读全部内容
4. 注意:页面标题、主要内容是否被识别?
### Landmark Navigation
1. 重复按 D 键
2. 检查:所有主要区域是否可达?
3. 检查:地标标签是否正确?
### Heading Navigation
1. 按 Insert + F7 → Headings
2. 检查:标题结构是否逻辑清晰?
3. 按 H 键导航标题
4. 检查:所有部分是否可发现?
### Form Testing
1. 按 F 键找到第一个表单字段
2. 检查:标签是否被朗读?
3. 填写无效数据
4. 提交表单
5. 检查:错误信息是否播报?
6. 检查:焦点是否移动到错误处?
### Interactive Elements
1. 使用 Tab 键遍历所有交互元素
2. 检查:每个元素是否播报其角色和状态?
3. 使用 Enter/Space 激活按钮
4. 检查:结果是否播报?
### Dynamic Content
1. 触发内容更新
2. 检查:变更是否播报?
3. 打开模态框
4. 检查:焦点是否被捕获?
5. 关闭模态框
6. 检查:焦点是否返回?
Start: Desktop shortcut or Ctrl + Alt + J
Virtual Cursor: Auto-enabled in browsers
Navigation:
Arrow keys 导航内容
Tab 下一个可聚焦元素
Insert + Down 朗读全部
Ctrl 停止朗读
Quick Keys:
H 下一个标题
T 下一个表格
F 下一个表单字段
B 下一个按钮
G 下一个图形
L 下一个列表
; 下一个地标
Forms Mode:
Enter 进入表单模式
Numpad + 退出表单模式
F5 列出表单字段
Lists:
Insert + F7 链接列表
Insert + F6 标题列表
Insert + F5 表单字段列表
Tables:
Ctrl + Alt + Arrows 表格导航
Enable: Settings → Accessibility → TalkBack
Toggle: Hold both volume buttons 3 seconds
Explore: 在屏幕上拖动手指
Next: 向右滑动
Previous: 向左滑动
Activate: 双击
Scroll: 双指滑动
Reading Controls (swipe up then right):
- Headings
- Links
- Controls
- Characters
- Words
- Lines
- Paragraphs
<!-- 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>
// 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);
}
}
<!-- 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>
<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>
// 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();
});
// 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",
});
}
每周安装量
3.3K
代码仓库
GitHub 星标
32.2K
首次出现
Jan 20, 2026
安全审计
安装于
claude-code2.6K
gemini-cli2.5K
opencode2.5K
cursor2.4K
codex2.3K
github-copilot2.1K
Practical guide to testing web applications with screen readers for comprehensive accessibility validation.
| 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% |
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)
| Mode | Purpose | When Used |
|---|---|---|
| Browse/Virtual | Read content | Default reading |
| Focus/Forms | Interact with controls | Filling forms |
| Application | Custom widgets | ARIA applications |
Enable: System Preferences → Accessibility → VoiceOver
Toggle: Cmd + F5
Quick Toggle: Triple-press Touch ID
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
## 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
<!-- 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>
Download: nvaccess.org
Start: Ctrl + Alt + N
Stop: Insert + Q
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
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
## NVDA Test Script
### Initial Load
1. Navigate to page
2. Let page finish loading
3. Press Insert + Down to read all
4. Note: Page title, main content identified?
### Landmark Navigation
1. Press D repeatedly
2. Check: All main areas reachable?
3. Check: Landmarks properly labeled?
### Heading Navigation
1. Press Insert + F7 → Headings
2. Check: Logical heading structure?
3. Press H to navigate headings
4. Check: All sections discoverable?
### Form Testing
1. Press F to find first form field
2. Check: Label read?
3. Fill in invalid data
4. Submit form
5. Check: Errors announced?
6. Check: Focus moved to error?
### Interactive Elements
1. Tab through all interactive elements
2. Check: Each announces role and state
3. Activate buttons with Enter/Space
4. Check: Result announced?
### Dynamic Content
1. Trigger content update
2. Check: Change announced?
3. Open modal
4. Check: Focus trapped?
5. Close modal
6. Check: Focus returns?
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
Enable: Settings → Accessibility → TalkBack
Toggle: Hold both volume buttons 3 seconds
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
<!-- 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>
// 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);
}
}
<!-- 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>
<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>
// 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();
});
// 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",
});
}
Weekly Installs
3.3K
Repository
GitHub Stars
32.2K
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code2.6K
gemini-cli2.5K
opencode2.5K
cursor2.4K
codex2.3K
github-copilot2.1K
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
102,200 周安装