Web 无障碍设计实践指南
去年项目被客户投诉”盲人无法使用”,才开始重视无障碍。现在成了团队的必须项。
为什么重要
| 原因 | 说明 |
|---|---|
| 法律要求 | 很多国家有法律要求 |
| 用户群体 | 全球 15% 人口有某种障碍 |
| SEO 好处 | 语义化对搜索引擎友好 |
| 代码质量 | 无障碍要求强迫你写规范代码 |
WCAG 原则
四大原则(POUR):
| 原则 | 英文 | 说明 |
|---|---|---|
| 可感知 | Perceivable | 信息必须能被感知 |
| 可操作 | Operable | 界面必须能被操作 |
| 可理解 | Understandable | 内容必须容易理解 |
| 健壮 | Robust | 兼容各种辅助技术 |
常见问题和解决
1. 图片缺少替代文本
<!-- 不好 -->
<img src="chart.png">
<!-- 好 -->
<img src="chart.png" alt="2024年销售额增长趋势图,从100万增长到150万">
原则:描述图片传达的信息,不是简单说”图片”。
2. 表单没有标签
<!-- 不好 -->
<input type="text" placeholder="用户名">
<!-- 好 -->
<label for="username">用户名</label>
<input type="text" id="username" name="username">
屏幕阅读器需要 label 来告诉用户这是什么字段。
3. 按钮语义不正确
<!-- 不好:用 div 当按钮 -->
<div onclick="submit()">提交</div>
<!-- 好 -->
<button type="button" onclick="submit()">提交</button>
div 没有按钮的语义,键盘无法聚焦,屏幕阅读器不会识别。
4. 键盘无法操作
<!-- 不好 -->
<div onclick="openModal()">打开弹窗</div>
<!-- 好 -->
<button onclick="openModal()">打开弹窗</button>
<!-- 或者添加键盘支持 -->
<div
onclick="openModal()"
onkeydown="if(event.key === 'Enter') openModal()"
tabindex="0"
role="button"
>
打开弹窗
</div>
ARIA 属性
ARIA 是 HTML 语义的补充。
常用属性
| 属性 | 用途 |
|---|---|
| role | 定义元素角色 |
| aria-label | 提供标签 |
| aria-labelledby | 关联标签 |
| aria-describedby | 关联描述 |
| aria-hidden | 隐藏辅助技术 |
| aria-expanded | 展开状态 |
| aria-live | 动态内容通知 |
示例:自定义下拉菜单
<div class="dropdown">
<button
id="dropdown-trigger"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="dropdown-menu"
>
选择选项
</button>
<ul
id="dropdown-menu"
role="listbox"
aria-labelledby="dropdown-trigger"
hidden
>
<li role="option" aria-selected="false">选项一</li>
<li role="option" aria-selected="true">选项二</li>
<li role="option" aria-selected="false">选项三</li>
</ul>
</div>
// 切换展开状态
function toggleDropdown() {
const menu = document.getElementById('dropdown-menu');
const trigger = document.getElementById('dropdown-trigger');
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
trigger.setAttribute('aria-expanded', !isExpanded);
menu.hidden = isExpanded;
}
示例:实时通知
<div aria-live="polite" aria-atomic="true" class="notifications">
<!-- 动态内容会被读出 -->
</div>
polite 表示不打断当前操作,assertive 会立即读出。
焦点管理
焦点顺序
Tab 键遍历顺序应该是逻辑顺序:
<!-- 不好:视觉顺序和 DOM 顺序不一致 -->
<div style="display: flex; flex-direction: row-reverse;">
<button>步骤3</button>
<button>步骤2</button>
<button>步骤1</button>
</div>
<!-- 好:调整 DOM 顺序 -->
<div>
<button>步骤1</button>
<button>步骤2</button>
<button>步骤3</button>
</div>
焦点可见
/* 不要移除焦点样式 */
button:focus {
outline: none; /* 不好 */
}
/* 自定义焦点样式 */
button:focus {
outline: 2px solid blue;
outline-offset: 2px;
}
模态框焦点
打开模态框后,焦点应该在模态框内。
function openModal(modal) {
modal.showModal();
// 焦点移到模态框
const firstFocusable = modal.querySelector('button, [href], input, select, textarea');
if (firstFocusable) {
firstFocusable.focus();
}
// 焦点陷阱
modal.addEventListener('keydown', trapFocus);
}
function trapFocus(e) {
if (e.key !== 'Tab') return;
const focusables = e.currentTarget.querySelectorAll('button, [href], input, select, textarea');
const first = focusables[0];
const last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
颜色对比度
文字和背景要有足够对比度。
| 级别 | 对比度要求 |
|---|---|
| AA(最低) | 4.5:1(普通文字)/ 3:1(大字) |
| AAA | 7:1(普通文字)/ 4.5:1(大字) |
检测工具:Chrome DevTools → Accessibility
检测工具
| 工具 | 用途 |
|---|---|
| Chrome DevTools | 即时检测 |
| axe DevTools | 浏览器插件 |
| Lighthouse | 综合评分 |
| WAVE | 页面可视化 |
| NVDA/VoiceOver | 屏幕阅读器测试 |
团队实践
代码审查清单
- [ ] 图片有 alt 属性
- [ ] 表单有 label
- [ ] 按钮使用正确语义
- [ ] 键盘可以操作
- [ ] 颜色对比度足够
- [ ] 焦点可见
- [ ] 动态内容有 aria-live
CI 检查
- name: Accessibility Check
run: npm run test:a11y
// 使用 axe-core
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('should have no accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
总结
无障碍不是额外工作,是开发的一部分。
关键点:
- 使用正确语义
- 支持键盘操作
- 提供替代文本
- 管理好焦点
- 保持足够对比度
做了无障碍后,代码质量也提高了。这是个良性循环。