name: accessibility-fundamentals description: 回顾可访问性,包括 WCAG、ARIA、键盘导航。当新手构建表单、按钮、模态框、交互元素,或询问“这可以访问吗”、“a11y”、“屏幕阅读器”时使用。
可访问性基础回顾
“可访问性不是一项功能,而是一个要求。如果 15% 的用户无法使用你的应用,你就失败了 15% 的用户。”
何时应用
激活此技能时:
- 审查包含按钮、链接或表单的 JSX
- 查看自定义交互组件
- 带有输入和标签的表单
- 导航菜单
- 模态对话框
- 任何用户交互代码
可访问性清单
必须有(每个交互元素)
- [ ] 键盘可访问 — 所有操作都可通过 Tab + Enter/Space 使用
- [ ] 焦点可见 — 聚焦元素的清晰视觉指示器
- [ ] 语义元素 — 使用
<button>而非<div onClick> - [ ] 表单标签 — 每个输入都有关联的
<label> - [ ] 替代文本 — 图像具有描述性的 alt 属性
- [ ] 足够对比度 — 文本在背景上可读(4.5:1 比率)
应该有(复杂交互)
- [ ] ARIA 标签 — 纯图标按钮有
aria-label - [ ] 焦点陷阱 — 模态框在关闭前捕获焦点
- [ ] 跳过链接 — 为键盘用户提供“跳转到主要内容”
- [ ] 活动区域 — 动态内容向屏幕阅读器宣布
- [ ] 错误消息 — 使用
aria-describedby链接到输入
切勿做
- [ ] 仅依赖颜色 — 颜色不应是唯一指示器
- [ ] 移除焦点轮廓 — 永远不要使用
outline: none而无替代 - [ ] 使用 div 作为按钮 — 使用语义
<button>或<a> - [ ] 困住用户 — 始终提供从模态框/菜单中退出的方式
常见错误(反模式)
1. Div 作为按钮
// ❌ 错误:不可键盘访问,无语义
<div onClick={handleClick} className="button">
点击我
</div>
// ✅ 良好:原生按钮元素
<button onClick={handleClick} className="button">
点击我
</button>
为何重要: <div onClick> 不接收键盘焦点,不响应 Enter/Space,屏幕阅读器不会宣布为按钮。
2. 缺少表单标签
// ❌ 错误:输入无标签
<input type="email" placeholder="Email" />
// ✅ 良好:标签链接到输入
<label htmlFor="email">Email</label>
<input id="email" type="email" placeholder="example@email.com" />
// ✅ 同样良好:包裹标签
<label>
Email
<input type="email" />
</label>
3. 纯图标按钮
// ❌ 错误:无可访问名称
<button onClick={handleDelete}>
<TrashIcon />
</button>
// ✅ 良好:屏幕阅读器的 ARIA 标签
<button onClick={handleDelete} aria-label="删除项目">
<TrashIcon aria-hidden="true" />
</button>
4. 移除焦点样式
/* ❌ 错误:焦点不可见 */
button:focus {
outline: none;
}
/* ✅ 良好:自定义但可见的焦点 */
button:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.6);
}
/* ✅ 最佳:使用 focus-visible */
button:focus-visible {
outline: 2px solid #4299e1;
outline-offset: 2px;
}
5. 非描述性链接文本
// ❌ 错误:“点击此处”未告知屏幕阅读器任何信息
<p>
阅读我们的隐私政策,<a href="/privacy">点击此处</a>。
</p>
// ✅ 良好:链接文本描述目的地
<p>
阅读我们的 <a href="/privacy">隐私政策</a>。
</p>
6. 缺少标题层级
// ❌ 错误:屏幕阅读器无法导航
<div className="title">欢迎</div>
<div className="subtitle">入门指南</div>
// ✅ 良好:正确的标题
<h1>欢迎</h1>
<h2>入门指南</h2>
苏格拉底式提问
用这些提问代替直接给出答案:
- 键盘:“你能仅使用键盘完成此操作吗?”
- 焦点:“如果我通过 Tab 键浏览页面,我能看到我在哪里吗?”
- 语义:“屏幕阅读器为这个元素宣布什么?”
- 标签:“如果占位符消失,用户如何知道要输入什么?”
- 颜色:“如果有人是色盲,他们还能理解这个 UI 吗?”
- 替代文本:“如果图像未加载,会丢失什么上下文?”
测试可访问性
手动测试
- 键盘测试:仅使用 Tab 键导航整个页面
- 焦点测试:你总能看见焦点在哪里吗?
- 缩放测试:布局在 200% 缩放时会破坏吗?
- 屏幕阅读器:尝试 VoiceOver(Mac)或 NVDA(Windows)
自动化测试
# 在你的测试文件中
# 模式:用于 React Testing Library 的 axe-core
import { axe } from 'jest-axe';
it('应该没有可访问性违规', async () => {
const { container } = render(<YourComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
ARIA 参考
常见 ARIA 属性
| 属性 | 使用场景 |
|---|---|
aria-label |
为纯图标按钮提供名称 |
aria-labelledby |
指向具有可见标签的元素 |
aria-describedby |
指向描述(错误消息) |
aria-hidden="true" |
向屏幕阅读器隐藏装饰性图标 |
aria-expanded |
指示下拉菜单/手风琴状态 |
aria-live |
宣布动态内容变化 |
role |
定义元素的用途(谨慎使用) |
ARIA 的第一条规则
“没有 ARIA 比糟糕的 ARIA 更好。”
首先使用语义 HTML。仅当 HTML 无法表达你需要的内容时使用 ARIA。
特定框架指南
React
// 模式:带有可访问名称的按钮
<button
onClick={handleAction}
aria-label="关闭模态框"
>
<XIcon aria-hidden="true" />
</button>
表单错误模式
// 模式:错误链接到输入
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-describedby={error ? "email-error" : undefined}
aria-invalid={error ? "true" : undefined}
/>
{error && (
<span id="email-error" role="alert">
{error}
</span>
)}
需要指出的危险信号
| 信号 | 提问 |
|---|---|
<div onClick> |
“键盘用户尝试点击这个时会发生什么?” |
outline: none |
“键盘用户如何知道他们在哪里?” |
| 无表单标签 | “屏幕阅读器如何知道这个输入是做什么的?” |
| 纯图标按钮 | “屏幕阅读器为这个按钮宣布什么?” |
| 颜色作为唯一指示器 | “如果有人是红绿色盲怎么办?” |
tabIndex > 0 |
“这破坏了自然的 Tab 顺序。为什么需要它?” |
面试联系
“我实施了可访问性最佳实践,包括语义 HTML、正确的表单标签和键盘导航,确保我们的应用对所有用户可用。”
STAR 故事素材:
- “识别了我们表单中的可访问性问题并修复了它们…”
- “在我们的模态组件中实施了正确的焦点管理…”
- “为我们的通知系统添加了屏幕阅读器支持…”
MCP 使用
Context7 - 框架文档
获取:WAI-ARIA 实践
获取:React 可访问性文档
Octocode - 真实示例
搜索:“aria-label” + “按钮” 模式
搜索:模态焦点陷阱实现
资源
- WCAG 2.1 指南(检查 Context7)
- Deque 的 axe-core 用于自动化测试
- WebAIM 颜色对比检查器