Web 可访问性:WCAG 指南与实践
可访问性不是"给少数人的福利",它是法律合规(ADA)、SEO 加分、产品质量。本文讲清 WCAG 的四原则(可感知/可操作/可理解/健壮)、最常见的前端 a11y 问题(对比度/焦点/alt/表单标签/ARIA 误用)、自动检查工具,以及把 a11y 当成 CI 的一部分。
本文要点
- 主题聚焦:React、TypeScript
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
很多团队把可访问性(a11y)当成"锦上添花"或"做善事",优先级永远排在功能后面。这是个昂贵的误解。本文讲清为什么必须做、依据什么做、实战里最常踩哪些坑、怎么用工具和流程兜住。
一、a11y 不是慈善,是合规底线 + SEO 加分 + 产品质量
先说合规。美国的 ADA(Americans with Disabilities Act)近几年关于网站可访问性的诉讼每年数千起,原告律所已经形成产业链——扫一眼你的网站,对比度不够、键盘不可达,发律师函,和解金额从几万到几十万美金不等。欧盟的 European Accessibility Act(EAA)2025 年 6 月已全面生效,覆盖电商、银行、交通等数字服务,违规面临各成员国各自的罚款。做 To B 或出海产品,这条线绕不过去。
再说 SEO。搜索引擎爬虫本质上就是个"瞎子用户":它看不见图片内容(靠 alt)、读不出 div 里的点击逻辑(靠语义标签)、跑不动 JS 交互(靠原生 HTML)。你为屏幕阅读器做的优化——语义化结构、描述性 alt、清晰标题层级——几乎 1:1 转化为 SEO 收益。Core Web Vitals 和无障碍的重叠度比你想象的更高。
最后是产品质量。a11y 强制你思考:这个交互键盘能用吗?焦点状态清晰吗?颜色对比够吗?这些约束倒逼出更健壮的 UI。一个 Tab 键走不通的页面,通常也意味着状态管理混乱、事件绑定脆弱。把 a11y 当质量门禁,等同于把一类 bug 挡在发布前。
一句话:a11y 不是"为少数人做慈善",它是法律合规、搜索流量、工程质量的交集。
二、WCAG 四原则(POUR):所有规则的源头
WCAG(Web Content Accessibility Guidelines)是 W3C 标准,当前主流是 2.1,2.2 已发布。它把规则归到四个原则下,记作 POUR:
- Perceivable 可感知:内容必须能被用户感知。盲人看不见 → 要有 alt/音频;色盲分不清 → 不能只用颜色传达信息;聋人听不见 → 视频要字幕。
- Operable 可操作:界面必须能被操作。键盘用户要能 Tab 到所有交互元素;不能有时间压力;不能有闪烁引发癫痫。
- Understandable 可理解:内容和操作必须可理解。语言要声明(
<html lang>);表单错误要有明确提示;组件行为要可预测。 - Robust 健壮:要能被各种用户代理(含辅助技术)可靠解析。HTML 要合法;ARIA 用法规范;兼容屏幕阅读器、语音控制等。
每个原则下有 Success Criteria,分三级:A(最低)、AA(主流目标,法律诉讼通常参照这一级)、AAA(最高,某些场景做不到)。实践中几乎所有团队的合理目标都是 WCAG 2.1 AA。别一上来追 AAA,性价比极低。
POUR 的真正价值在于:遇到没见过的场景,你可以回到四原则去推导,而不是死记几十条规则。比如"加载状态要不要通知屏幕阅读器?"——回到 Operable 和 Perceivable,答案自然是:要,否则用户不知道发生了什么。
三、最常见的前端 a11y 问题(及修法)
实战中,90% 的问题集中在五类。
1. 图片缺 alt,或装饰图没标 aria-hidden
// ❌ 坏:信息图没 alt,屏幕阅读器只能读文件名;装饰图反而念废话
<img src="banner.jpg" />
<img src="icon-check.png" /> <span>已保存</span>
<img src="decorator-line.svg" alt="一条装饰用的弯线" />
// ✅ 好:信息图给描述,装饰图空 alt + aria-hidden
<img src="banner.jpg" alt="2026 春季新品发布会主视觉" />
<img src="icon-check.png" alt="" aria-hidden="true" /> <span>已保存</span>
<img src="decorator-line.svg" alt="" aria-hidden="true" />
规则:有信息的图 → 描述性 alt;纯装饰 → alt="" 加 aria-hidden。 别干脆不写 alt 属性——那是另一回事,屏幕阅读器会尝试读 url。
2. 对比度不够(尤其灰字、低饱和配色)
WCAG AA 要求:正文 ≥ 4.5:1,大字(18pt+/14pt 粗体)≥ 3:1。设计稿里常见的 #999 on #fff(2.85:1)不达标。
// ❌ 坏:对比度 2.85,不达标
<p className="text-gray-400">次要说明文字</p>
// ✅ 好:对比度 7.4,远超 AA
<p className="text-gray-600">次要说明文字</p>
这事得在设计阶段就卡住:把设计系统所有色值跑一遍对比度检查,不达标的 token 直接打回。Stark、Polypane、Figma 的 A11y 插件都能做。
3. 键盘不可达:tab 顺序、焦点不可见、自定义组件没键盘支持
这是最严重的一类,影响所有键盘用户(不只残障)。
焦点不可见的典型——全局干掉 outline 是 a11y 重罪:
// ❌ 坏
* { outline: none; }
// ✅ 好:只在鼠标点击时隐藏,键盘聚焦保留
:focus:not(:focus-visible) { outline: none; }
:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
自定义组件没键盘支持更常见:用 <div onClick> 模拟按钮,键盘用户 Tab 不进去,更别提 Enter/Space 触发。
// ❌ 坏:div 不是按钮,键盘和屏幕阅读器全废
<div onClick={handleSubmit} className="btn">提交</div>
// ✅ 好:原生 button 自带 Tab、Enter、Space、焦点、role
<button onClick={handleSubmit} className="btn">提交</button>
做 Modal、Dropdown、Tabs、Combobox 这类复合组件时,别自己 invent 键盘语义,直接抄 WAI-ARIA Authoring Practices Guide(APG)的模式,那是经过验证的。
4. 表单缺 label / 错误提示不关联
// ❌ 坏:占位符当 label,屏幕阅读器读不出字段含义
<input placeholder="邮箱" />
// ✅ 好:label 显式关联
<label htmlFor="email">邮箱</label>
<input id="email" type="email" aria-required="true" />
错误提示更要关联,否则用户不知道为什么提交失败:
<input
id="email"
aria-invalid="true"
aria-describedby="email-error"
/>
<p id="email-error" role="alert">请输入有效邮箱</p>
aria-describedby 把错误文本绑给输入框,role="alert" 让屏幕阅读器主动播报。
5. ARIA 滥用:乱加 role/aria-label 反而误导屏幕阅读器
ARIA 的第一定律:No ARIA is better than bad ARIA。 错误的 ARIA 比没有更糟,因为它主动撒谎。
// ❌ 坏:给原生 button 加 role,重复且可能冲突
<button role="button" aria-label="按钮">提交</button>
// ❌ 坏:aria-label 覆盖可见文本,语音控制用户念"提交"没反应
<button aria-label="确认提交">提交</button>
// ❌ 坏:用 ARIA 造组件却不实现键盘交互
<div role="tab" tabIndex={0}>详情</div> {/* 缺 ArrowLeft/Right 处理 */}
记住:ARIA 不会给元素加任何行为,它只改语义。 你写 role="tab",屏幕阅读器会念"标签页",但 ArrowLeft/Right 切换、aria-selected 状态、tabindex 管理,全得你自己实现。90% 的情况,用对语义 HTML 就不需要 ARIA。
四、第一原则:语义 HTML 优先,ARIA 是补充
把上面五类问题抽象,根因往往是:用通用元素(div/span)加 CSS 和 JS,重新发明了浏览器已经给你的东西。
| 你想要的 | 别用 | 用 |
|---|---|---|
| 按钮 | <div onClick> | <button> |
| 导航 | <div class="nav"> | <nav> |
| 主内容 | <div id="app"> | <main> |
| 标题层级 | <div class="h1"> | <h1>~<h6> |
| 表单字段 | <div> + 占位符 | <input> + <label> |
| 列表 | <div> 们 | <ul>/<ol> + <li> |
语义 HTML 自带键盘支持、焦点管理、屏幕阅读器播报、Landmark 导航。屏幕阅读器用户可以一键跳到 <main>、列出所有 <h2>、跳过 <nav>——这些快捷键全基于语义标签。你全用 div,等于把他们的导航系统废掉。
判断准则:能不改 HTML 用 a11y 就达成的,不要加 ARIA。 一个原生 <button> 已经是无障碍的;你在上面堆 role="button" aria-label="按钮",除了暴露你不懂 ARIA,没有任何作用。
五、自动检查:把 a11y 当一等公民
光靠自觉不行,得有工具兜底。三件套:
1. eslint-plugin-jsx-a11y——静态检查,在编辑器里就拦截。配进 ESLint,alt 缺失、onClick 没 onKeyDown、tabindex 滥用等在写代码时就能发现。成本最低,收益最快。
2. @axe-core/playwright(或 axe-core/webdriverjs)——运行时检查,在真实渲染的 DOM 上跑。axe-core 是 Deque 开源的规则引擎,业界事实标准。集成到组件测试或 E2E:
import { expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('Modal 无 a11y 违规', async ({ page }) => {
await page.goto('/settings');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
});
3. Lighthouse a11y audit——背后也是 axe-core,但带可读报告和分数。适合做整体基线,@lhci/cli 可以把分数变化 track 起来,回归一目了然。
关键认知:自动工具只能发现约 30–40% 的 a11y 问题(对比度、缺失 alt、ARIA 语法错等"机器可判定"的)。键盘流程是否合理、屏幕阅读器体验是否顺畅,工具查不出来。所以自动检查是必要不充分条件。
六、手动测试 + 进 CI + 设计阶段卡
手动测试两件事,省不掉:
-
键盘测试:拔鼠标,只用 Tab / Shift+Tab / Enter / Space / 方向键 / Esc 把主流程跑一遍。能不能到所有交互元素?焦点看得见吗?顺序符合视觉顺序?Modal 打开时焦点有没有困在外面?零成本,十分钟搞定,能暴露 80% 的操作性 bug。
-
屏幕阅读器测试:Mac 用 VoiceOver(Cmd+F5 开),Windows 用免费开源的 NVDA。跑一遍核心流程,听播报是否合理。不用成专家,会基本操作 + 听得懂"标题列表""地标列表"就够了。
进 CI:把 ESLint a11y 规则、axe-core 关键页测试、Lighthouse a11y 分数门槛接进 CI,任何 PR 不通过就不让合。门禁机制比任何文档都有效——文档写得再漂亮,不如一个红色 ×。
# .github/workflows/a11y.yml 简化示意
- run: pnpm lint # 含 eslint-plugin-jsx-a11y
- run: pnpm test:e2e:a11y # axe-core 关键页
- run: pnpm lhci a11y --score=90 # Lighthouse a11y 不低于 90
设计阶段就考虑:最便宜的修复在设计稿阶段。对比度不达标的配色、纯颜色传达状态、过小的点击区域、没有 focus 样式的组件库——这些在 Figma 里改一行,比上线后返工便宜 100 倍。把"对比度 ≥ 4.5:1""所有交互元素有 focus 态""最小点击区域 44×44px"写进设计规范,设计评审时就卡。
小结
- a11y 是合规、SEO、质量的交集,不是慈善。 ADA/EAA 是法律底线,SEO 和工程质量是商业收益,所有理由都指向"必须做"。
- 所有规则源于 POUR 四原则:可感知、可操作、可理解、健壮。记不住细则时回到原则推导;团队目标统一锁 WCAG 2.1 AA。
- 90% 的问题集中在五类:图片 alt、对比度、键盘可达、表单 label、ARIA 滥用。其中"用 div 模拟交互元素"是万恶之源。
- 语义 HTML 优先,ARIA 是补充不是替代。 原生
<button>/<nav>/<main>自带无障碍能力,先想清楚用对标签,再考虑 ARIA。No ARIA is better than bad ARIA。 - 自动检查是必要不充分:ESLint + axe-core + Lighthouse 进 CI 当门禁;键盘测试和屏幕阅读器测试补上工具发现不了的那 60–70%。
金句:无障碍不是"做完功能再补的东西",它是你页面健壮性的镜子——Tab 键走不通的页面,通常也是个烂页面。 把 a11y 当一等公民,从设计到 CI 全链路卡住,你会发现它不是负担,而是质量护城河。
Related