无障碍基础(Accessibility Fundamentals)
一、为什么无障碍不是"可选项"
1.1 数字世界的通行权
全球约 15% 的人口(超过 10 亿人)有某种形式的残障。Web 无障碍确保这些用户能够平等地获取信息和使用服务。更重要的是,残障不仅仅是永久性的:
| 类型 | 永久性 | 临时性 | 情境性 |
|---|---|---|---|
| 视觉 | 失明 | 眼部手术恢复 | 阳光下看手机 |
| 运动 | 肢体缺失 | 手臂骨折 | 抱着婴儿 |
| 听觉 | 失聪 | 耳部感染 | 嘈杂环境 |
| 认知 | 学习障碍 | 脑震荡 | 疲劳/压力 |
每个人在生命中都会经历某种形式的"残障"——无障碍设计惠及所有用户。
1.2 法律与合规
许多国家/地区有强制的无障碍法规:
- 美国 ADA(Americans with Disabilities Act)
- 欧盟 European Accessibility Act(2025 年生效)
- 中国《无障碍环境建设法》(2023 年施行)
不合规可能面临法律诉讼——近年来 Web 无障碍相关诉讼数量激增。
二、WCAG 与 POUR 原则
2.1 WCAG 概述
Web Content Accessibility Guidelines(WCAG) 是 W3C 制定的无障碍标准,当前版本为 WCAG 2.2,分三个合规级别:
| 级别 | 要求 | 说明 |
|---|---|---|
| A | 最低要求 | 基本可用性保障 |
| AA | 行业标准 | 大多数法规要求达到此级别 |
| AAA | 最高标准 | 通常不作为全站强制要求 |
2.2 四大原则(POUR)
WCAG 围绕四个核心原则组织:
P — Perceivable(可感知) 信息和界面组件必须以用户可以感知的方式呈现。
<!-- 图片替代文本 -->
<img src="chart.png" alt="2024年第三季度销售增长15%的柱状图" />
<!-- 装饰性图片 -->
<img src="divider.png" alt="" role="presentation" />
<!-- 视频字幕 -->
<video controls>
<source src="talk.mp4" type="video/mp4" />
<track kind="captions" src="captions-zh.vtt" srclang="zh" label="中文字幕" />
</video>
O — Operable(可操作) 用户必须能够操作界面组件和导航。
<!-- 所有交互元素必须可键盘操作 -->
<button onclick="toggle()">展开详情</button> <!-- ✅ 原生可聚焦 -->
<div onclick="toggle()">展开详情</div> <!-- ❌ 不可聚焦 -->
<!-- 如果必须使用 div,需要完整的键盘支持 -->
<div role="button" tabindex="0"
onclick="toggle()"
onkeydown="if(event.key==='Enter'||event.key===' ')toggle()">
展开详情
</div>
U — Understandable(可理解) 信息和用户界面的操作必须可理解。
<!-- 声明页面语言 -->
<html lang="zh-CN">
<!-- 内联语言切换 -->
<p>这是一个 <span lang="en">responsive design</span> 的例子。</p>
<!-- 表单错误说明 -->
<input type="email" aria-invalid="true" aria-describedby="err" />
<p id="err" role="alert">请输入有效的邮箱地址,例如 [email protected]</p>
R — Robust(健壮性) 内容必须足够健壮,可以被各种用户代理(包括辅助技术)可靠地解析。
<!-- 使用语义化 HTML -->
<nav aria-label="主导航">...</nav>
<main>...</main>
<aside>...</aside>
<!-- 正确的 ARIA 状态管理 -->
<button aria-expanded="false" aria-controls="menu">菜单</button>
<ul id="menu" hidden>...</ul>
三、键盘导航
3.1 焦点管理
键盘导航是无障碍的基石——许多用户无法使用鼠标:
原生可聚焦元素(自动进入 Tab 序列):
<a href> <button> <input> <select> <textarea> <details>
不可聚焦元素:
<div> <span> <p> <section> <h1>-<h6>
tabindex 的三种值:
| 值 | 行为 |
|---|---|
tabindex="0" |
加入自然 Tab 序列 |
tabindex="-1" |
可通过 JS .focus() 聚焦,但不在 Tab 序列中 |
tabindex="正数" |
❌ 强制改变 Tab 顺序——几乎总是错误的做法 |
3.2 焦点可见性
/* ❌ 绝对禁止——移除焦点指示器 */
*:focus { outline: none; }
/* ✅ 自定义焦点样式 */
:focus-visible {
outline: 2px solid #4A90D9;
outline-offset: 2px;
border-radius: 2px;
}
/* :focus-visible 只在键盘导航时显示,鼠标点击时不显示 */
3.3 焦点陷阱(Focus Trap)
模态对话框需要将焦点限制在其内部:
function trapFocus(element) {
const focusable = element.querySelectorAll(
'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
element.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
first.focus();
}
四、屏幕阅读器
4.1 屏幕阅读器如何"看"页面
屏幕阅读器依赖可访问性树(Accessibility Tree)——浏览器从 DOM 生成的平行结构,只包含对辅助技术有意义的信息:
DOM 树 → 可访问性树
<nav> → navigation landmark
<ul> → list (3 items)
<li><a href="/">首页</a></li> → link "首页"
4.2 常见屏幕阅读器
| 阅读器 | 平台 | 浏览器配对 |
|---|---|---|
| NVDA | Windows(免费) | Firefox |
| JAWS | Windows(付费) | Chrome |
| VoiceOver | macOS / iOS(内置) | Safari |
| TalkBack | Android(内置) | Chrome |
4.3 开发中测试
macOS 快速测试:
1. Cmd + F5 开启 VoiceOver
2. Ctrl + Option + → 逐元素浏览
3. Ctrl + Option + U 打开转子(Rotor)查看 Landmarks、Headings 等
五、颜色与对比度
5.1 WCAG 对比度要求
| 文本类型 | AA 级别 | AAA 级别 |
|---|---|---|
| 正常文本(< 18px) | 4.5:1 | 7:1 |
| 大文本(≥ 18px 或 14px bold) | 3:1 | 4.5:1 |
| 非文本元素(图标、边框等) | 3:1 | - |
5.2 不要只靠颜色传达信息
<!-- ❌ 只用颜色区分状态 -->
<span style="color: red">错误</span>
<span style="color: green">成功</span>
<!-- ✅ 颜色 + 图标 + 文字 -->
<span class="error">
<svg aria-hidden="true"><!-- 错误图标 --></svg>
错误:邮箱格式不正确
</span>
<span class="success">
<svg aria-hidden="true"><!-- 成功图标 --></svg>
成功:信息已保存
</span>
5.3 工具检查
- Chrome DevTools → Rendering → "Emulate vision deficiencies"
- WebAIM Contrast Checker
- Lighthouse 无障碍审计
六、图片与替代文本
6.1 alt 属性决策树
图片是否传达信息?
├─ 是 → 提供描述性 alt
│ ├─ 简单图片:alt="一只金色拉布拉多犬在公园中奔跑"
│ ├─ 图表/数据:alt="2024年Q3收入同比增长15%"(概述数据含义)
│ └─ 功能性图片(按钮/链接中):alt="搜索" / alt="关闭对话框"
│
├─ 否(纯装饰)→ alt=""(空字符串,不是省略 alt)
│
└─ 复杂图片 → alt 简述 + 长描述
<figure>
<img alt="公司组织架构概览" src="org-chart.png" />
<figcaption>详细的组织架构说明文字...</figcaption>
</figure>
6.2 常见错误
<!-- ❌ 文件名作为 alt -->
<img alt="IMG_20240301.jpg" src="photo.jpg" />
<!-- ❌ 冗余描述 -->
<img alt="图片:一只猫" src="cat.jpg" /> <!-- "图片"是多余的 -->
<!-- ❌ 省略 alt(屏幕阅读器会读出文件名)-->
<img src="chart.png" /> <!-- 阅读器:"图片 chart dot png" -->
<!-- ✅ 正确做法 -->
<img alt="季度销售趋势图,显示 Q3 增长最快" src="chart.png" />
七、Live Region(实时区域)
当页面内容动态更新时,需要通知屏幕阅读器:
<!-- aria-live="polite" — 等阅读器空闲时播报 -->
<div aria-live="polite" id="status">
<!-- JS 更新这里的内容时,阅读器会播报 -->
</div>
<!-- aria-live="assertive" — 立即中断播报 -->
<div aria-live="assertive" id="error">
<!-- 用于紧急错误信息 -->
</div>
<!-- role="alert" 隐式 aria-live="assertive" -->
<div role="alert">操作失败,请重试</div>
<!-- role="status" 隐式 aria-live="polite" -->
<div role="status">已保存 3 个文件</div>
<!-- role="log" 用于聊天记录等追加内容 -->
<div role="log" aria-live="polite">
<!-- 新消息追加到这里 -->
</div>
八、跳过导航(Skip Navigation)
<body>
<!-- 首个可聚焦元素:跳转链接 -->
<a href="#main-content" class="skip-link">
跳到主要内容
</a>
<header>
<nav><!-- 导航菜单(可能很长)--></nav>
</header>
<main id="main-content" tabindex="-1">
<!-- 页面主要内容 -->
</main>
</body>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
z-index: 100;
padding: 8px 16px;
background: #000;
color: #fff;
}
.skip-link:focus {
top: 0; /* Tab 聚焦时显示 */
}
</style>
九、自动化测试与审计
9.1 工具链
# axe-core — 最流行的无障碍测试引擎
npx @axe-core/cli https://localhost:3000
# Lighthouse CLI
npx lighthouse https://localhost:3000 --only-categories=accessibility
# pa11y — 另一个无障碍测试工具
npx pa11y https://localhost:3000
9.2 eslint-plugin-jsx-a11y(React 项目)
{
"plugins": ["jsx-a11y"],
"rules": {
"jsx-a11y/alt-text": "error",
"jsx-a11y/anchor-is-valid": "error",
"jsx-a11y/click-events-have-key-events": "error",
"jsx-a11y/no-static-element-interactions": "error",
"jsx-a11y/label-has-associated-control": "error"
}
}
9.3 自动化无法覆盖的部分
自动化工具只能发现约 30-50% 的无障碍问题。以下必须手动测试:
- 焦点顺序是否合理(Tab 序列是否符合视觉流)
- 自定义组件的键盘操作(如自定义下拉菜单)
- 动态内容更新是否正确播报
- alt 文本是否真正有意义(而非仅仅"存在")
- 整体用户流程是否可用(用键盘完成注册流程)
十、无障碍审计清单
- ☐ 所有图片是否有适当的
alt属性? - ☐ 标题层级(h1-h6)是否连续且有意义?
- ☐ 所有表单控件是否有关联的
<label>? - ☐ 颜色对比度是否满足 WCAG AA(4.5:1)?
- ☐ 是否仅靠颜色传达信息?
- ☐ 所有功能是否可用键盘操作?
- ☐ 焦点指示器是否可见?
- ☐ 模态对话框是否正确管理焦点?
- ☐ 动态内容更新是否有
aria-live通知? - ☐ 页面是否有合理的 Landmark 结构?
参考资料
- web.dev — Accessibility
- WAI-ARIA Authoring Practices
- WebAIM — WCAG Checklist
- A11y Project Checklist
- Inclusive Components — Heydon Pickering