React 无障碍访问 a11y
React 无障碍访问 a11y:让每个用户都能用你的应用
引言
无障碍(Accessibility,缩写 a11y)不是「加分项」,而是产品的底线:全球有超过十亿人存在不同程度的视觉、听觉、运动或认知障碍。对 React 应用而言,无障碍意味着三件事:语义化的 HTML、键盘可操作、屏幕阅读器可理解。本文将从无障碍的四大原则讲起,系统梳理 React 中的语义化、键盘导航、ARIA 用法、焦点管理与自动化测试。
一、无障碍的四大原则(POUR)
WAI-ARIA 与 WCAG 定义了无障碍的四大原则:可感知(Perceivable)、可操作(Operable)、可理解(Understandable)、健壮性(Robust)。
POUR 原则模型
flowchart TB
classDef p1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef p2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
classDef p3 fill:#e0f7fa,stroke:#00838f,color:#004d40
A[无障碍四大原则] --> B[可感知<br/>内容能被感知]
A --> C[可操作<br/>界面能操作]
A --> D[可理解<br/>信息可理解]
A --> E[健壮性<br/>技术可兼容]
B --> B1[替代文本/对比度/字幕]
C --> C1[键盘可达/足够时间]
D --> D1[可预测/输入辅助]
E --> E1[辅助技术兼容]
click B "https://www.w3.org/WAI/WCAG22/Understanding/perceivable" "WCAG 文档"
class A p1
class B p1
class C p2
class D p2
class E p2
class B1 p3
class C1 p3
class D1 p3
class E1 p3
二、语义化 HTML:无障碍的基石
React 组件的最大无障碍风险是语义丢失:div 嵌套、button 换 div、列表不用 ul。屏幕阅读器依赖语义元素导航,语义化是「零成本」的无障碍投资。
语义化对照
flowchart LR
classDef s1 fill:#ffebee,stroke:#c62828,color:#b71c1c
classDef s2 fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
subgraph Bad[反模式]
direction LR
B1["<div onClick> 按钮"] --> B2["<div> 标题"]
B2 --> B3["<div><div> 列表"]
end
subgraph Good[语义化]
direction LR
G1["<button> 按钮"] --> G2["<h1><h2> 标题"]
G2 --> G3["<ul><li> 列表"]
G1 --> G4["<nav><main><aside> 区域"]
end
click G1 "https://react.dev/reference/react-dom/components/button" "button 组件文档"
class Bad s1
class B1 s1
class B2 s1
class B3 s1
class Good s2
class G1 s2
class G2 s2
class G3 s2
class G4 s2
React 中保持语义的实践
javascript123456789101112131415// ✅ 按钮用 button <button onClick={handleClick}>保存</button> // ❌ 别用 div 模拟 <div onClick={handleClick} role="button">保存</div> // ✅ 列表用 ul/li <ul> {items.map(item => <li key={item.id}>{item.name}</li>)} </ul> // ✅ 图片必须带 alt <img src={avatar} alt={`${user.name} 的头像`} /> // 装饰性图片用空 alt <img src={decoration} alt="" />
关键:div 换成语义元素通常不改样式(配合 reset CSS),但屏幕阅读器的体验天差地别。
三、键盘可操作性:Tab 顺序与焦点管理
键盘用户(视力障碍、运动障碍)完全依赖键盘操作。React 应用最常见的键盘问题:焦点丢失、Tab 顺序错乱、快捷键无说明。
键盘导航模型
flowchart TD
classDef k1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef k2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
classDef k3 fill:#e0f7fa,stroke:#00838f,color:#004d40
A[键盘操作] --> B[Tab 键<br/>焦点前进]
A --> C[Shift+Tab<br/>焦点后退]
A --> D[Enter/Space<br/>激活按钮]
A --> E[方向键<br/>组件内导航]
B --> F{焦点可见性}
F -->|有样式| G[键盘用户可追踪]
F -->|被移除| H[键盘用户迷失]
E --> I[组合框/列表内导航]
click B "https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html" "键盘可达性文档"
class A k1
class B k1
class C k1
class D k1
class E k1
class F k2
class G k3
class H k3
class I k2
焦点管理的最佳实践
- 不要移除焦点样式:
outline: none需要配替代方案; - 路由切换管理焦点:页面标题聚焦或「跳过导航」链接;
- 弹窗焦点陷阱:参考本站《Portals 与弹窗》章节;
- 动态内容聚焦:新内容出现时用 ref 聚焦。
javascript12345678910111213// 页面切换时聚焦主标题 function Page({ title, children }) { const headingRef = useRef(null); useEffect(() => { headingRef.current?.focus({ preventScroll: true }); }, [title]); return ( <main> <h1 ref={headingRef} tabIndex={-1}>{title}</h1> {children} </main> ); }
四、ARIA:当语义不够时的补充
ARIA(Accessible Rich Internet Applications)为自定义组件补充无障碍语义。核心心智:first rule of ARIA 是「能不用就不用」——优先原生元素,ARIA 只补缺口。
ARIA 的使用原则
flowchart TD
classDef a1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef a2 fill:#fff3e0,stroke:#f57c00,color:#e65100
classDef a3 fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
A[需要无障碍语义] --> B{原生元素是否可用}
B -->|是| C[使用原生元素<br/>不需 ARIA]
B -->|否 自定义组件| D{需要补充什么}
D -->|角色| E[role 属性]
D -->|命名| F[aria-label / labelledby]
D -->|状态| G[aria-expanded / checked]
D -->|关系| H[aria-controls / describedby]
click E "https://www.w3.org/TR/wai-aria-1.2/" "ARIA 规范文档"
class A a1
class B a1
class C a3
class D a2
class E a3
class F a3
class G a3
class H a3
常见 ARIA 用法示例
javascript1234567891011121314151617181920// 可展开按钮 <button aria-expanded={isOpen} aria-controls="panel-id" > 高级选项 </button> <div id="panel-id" hidden={!isOpen}>...</div> // 错误提示关联 <input aria-invalid={hasError} aria-describedby={hasError ? "error-id" : undefined} /> <span id="error-id" role="alert">输入有误</span> // 进度条 <div role="progressbar" aria-valuenow={percent} aria-valuemin={0} aria-valuemax={100}> {percent}% </div>
五、表单与动态内容的无障碍
表单是无障碍问题的重灾区:标签关联、错误提示、必填标识。动态内容(Toast、错误消息)需要及时通知屏幕阅读器。
表单与动态通知
flowchart TB
classDef f1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef f2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
classDef f3 fill:#e0f7fa,stroke:#00838f,color:#004d40
A[表单无障碍] --> B[label 关联<br/>htmlFor 或包裹]
A --> C[错误提示<br/>aria-describedby]
A --> D[必填标识<br/>required + aria-required]
E[动态通知] --> F[role=alert<br/>立即播报]
E --> G[role=status<br/>礼貌播报]
E --> H[aria-live 区域<br/>自定义播报]
click B "https://react.dev/reference/react-dom/components/input" "input 标签关联文档"
class A f1
class B f2
class C f2
class D f2
class E f1
class F f3
class G f3
class H f3
表单标签关联示例
javascript12345678910111213function LoginForm() { return ( <form onSubmit={handleSubmit}> <label htmlFor="email">邮箱</label> <input id="email" type="email" required aria-describedby="email-hint" /> <span id="email-hint">用于登录与找回密码</span> <label htmlFor="password">密码</label> <input id="password" type="password" required /> <button type="submit">登录</button> </form> ); }
六、无障碍的自动化检查
无障碍不能只靠「自觉」——自动化工具在 CI 中拦截回归是无障碍工程的底线。
检查工具链
flowchart LR
classDef c1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef c2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
classDef c3 fill:#e0f7fa,stroke:#00838f,color:#004d40
A[开发阶段<br/>eslint-plugin-jsx-a11y] --> D[静态规则检查]
B[测试阶段<br/>jest-axe 断言] --> E[运行时 DOM 检查]
C[构建阶段<br/>axe-core 扫描] --> F[页面级检查]
D --> G[CI 集成]
E --> G
F --> G
G --> H{违规拦截}
click A "https://github.com/jsx-eslint/eslint-plugin-jsx-a11y" "jsx-a11y 插件文档"
click E "https://github.com/nickcolley/jest-axe" "jest-axe 文档"
class A c1
class B c1
class C c1
class D c2
class E c2
class F c2
class G c3
class H c3
测试示例
javascript12345678910import { render } from "@testing-library/react"; import { axe, toHaveNoViolations } from "jest-axe"; expect.extend(toHaveNoViolations); test("页面无无障碍违规", async () => { const { container } = render(<App />); const results = await axe(container); expect(results).toHaveNoViolations(); });
七、无障碍的常见误区
flowchart TD
classDef w1 fill:#ffebee,stroke:#c62828,color:#b71c1c
classDef w2 fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
A[常见误区] --> B[ARIA 万能论<br/>乱用 role 反而误导]
A --> C[移除 outline<br/>键盘用户迷失]
A --> D[颜色传达状态<br/>色盲用户无法分辨]
A --> E[只测自动化<br/>忽略真实体验]
B --> F[原生优先 克制用 ARIA]
C --> G[保留焦点环 样式可见]
D --> H[图标 + 文字 + 纹理辅助]
E --> I[自动化 + 人工走查结合]
class A w1
class B w1
class C w1
class D w1
class E w1
class F w2
class G w2
class H w2
class I w2
八、总结
React 无障碍是一套「默认正确 + 自动化保障」的工程体系:
- 语义化:原生元素优先,div 化是最大风险;
- 键盘:焦点可见、顺序合理、导航可达;
- ARIA:先语义后 ARIA,只补缺口;
- 表单动态:标签关联 + role 播报;
- 自动化:lint + axe 测试 + CI 门禁三层保障。
实践建议:把 eslint-plugin-jsx-a11y 作为强制规则;新组件必须过 axe 检查;每月用屏幕阅读器(NVDA / VoiceOver)人工走查核心流程。无障碍不是「为少数人做的妥协」,而是产品质量的度量。下一篇我们将深入 Vite 构建工具链与模块热更新。
