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 中保持语义的实践

关键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

焦点管理的最佳实践

  1. 不要移除焦点样式outline: none 需要配替代方案;
  2. 路由切换管理焦点:页面标题聚焦或「跳过导航」链接;
  3. 弹窗焦点陷阱:参考本站《Portals 与弹窗》章节;
  4. 动态内容聚焦:新内容出现时用 ref 聚焦。

四、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 用法示例

五、表单与动态内容的无障碍

表单是无障碍问题的重灾区:标签关联、错误提示、必填标识。动态内容(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

表单标签关联示例

六、无障碍的自动化检查

无障碍不能只靠「自觉」——自动化工具在 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

测试示例

七、无障碍的常见误区

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 无障碍是一套「默认正确 + 自动化保障」的工程体系:

  1. 语义化:原生元素优先,div 化是最大风险;
  2. 键盘:焦点可见、顺序合理、导航可达;
  3. ARIA:先语义后 ARIA,只补缺口;
  4. 表单动态:标签关联 + role 播报;
  5. 自动化:lint + axe 测试 + CI 门禁三层保障。

实践建议:把 eslint-plugin-jsx-a11y 作为强制规则;新组件必须过 axe 检查;每月用屏幕阅读器(NVDA / VoiceOver)人工走查核心流程。无障碍不是「为少数人做的妥协」,而是产品质量的度量。下一篇我们将深入 Vite 构建工具链与模块热更新。