无障碍基础

系统理解 Web 无障碍的核心原则(POUR)、WCAG 标准与工程实践,构建所有人都可使用的 Web 应用。

无障碍基础(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% 的无障碍问题。以下必须手动测试:

  1. 焦点顺序是否合理(Tab 序列是否符合视觉流)
  2. 自定义组件的键盘操作(如自定义下拉菜单)
  3. 动态内容更新是否正确播报
  4. alt 文本是否真正有意义(而非仅仅"存在")
  5. 整体用户流程是否可用(用键盘完成注册流程)

十、无障碍审计清单

  1. ☐ 所有图片是否有适当的 alt 属性?
  2. ☐ 标题层级(h1-h6)是否连续且有意义?
  3. ☐ 所有表单控件是否有关联的 <label>
  4. ☐ 颜色对比度是否满足 WCAG AA(4.5:1)?
  5. ☐ 是否仅靠颜色传达信息?
  6. ☐ 所有功能是否可用键盘操作?
  7. ☐ 焦点指示器是否可见?
  8. ☐ 模态对话框是否正确管理焦点?
  9. ☐ 动态内容更新是否有 aria-live 通知?
  10. ☐ 页面是否有合理的 Landmark 结构?

参考资料

延展阅读