前端无障碍
无障碍性是每次 UI 变更的一部分——与之共同设计、构建和测试。UI 任务直到以下条件都满足才算完成:仅使用键盘即可操作;每个控件宣布其名称、角色和状态;自动化检查通过;测试通过角色进行查询。
目标:WCAG 2.2 AA 级别——在欧盟,《欧洲无障碍法案》(各国法律如 BFSG、EN 301 549)使其对许多产品具有强制性。发现的问题引用 WCAG A/AA 标准或辅助技术用户无法完成的特定任务。AAA 标准和惯例(如一个 <h1>、44px 目标)是建议,而非阻止项。
| 任务 | 参考文件 |
|---|---|
| 选择元素、地标、标题、ARIA、可访问名称、禁用状态 | references/semantics-and-aria.md |
| 焦点环、Tab 顺序、跳转链接、对话框、路由变更、小部件键盘映射 | references/focus-and-keyboard.md |
| 标签、自动填充、输入类型、验证错误、提交行为 | references/forms.md |
| 宣布变更、实时区域、通知、替代文本、SVG、媒体 | references/screen-readers.md |
| 对比度、颜色、目标尺寸、动画、缩放、重排、内容处理 | references/visual-and-motion.md |
| 自动化和手动测试、CI | references/testing.md |
| 审计/审查现有 UI、报告格式 | references/audit.md |
| 查询 WCAG 2.2 标准 | references/wcag.md |
核心规则——始终遵循
优先使用原生元素。 使用 <button> 表示操作,<a href> / 路由器 <Link> 表示导航,使用 <label>、<fieldset>、<table>、<dialog> 和地标。绝不要使用 <div onClick>。没有 ARIA 比使用糟糕的 ARIA 更好;如果一个角色被设置,它需要实现完整的键盘模型。复杂的小部件(组合框、菜单、日期选择器)应来自项目可访问的无头库,而不是手工构建。
每个交互元素都必须有可访问名称 [4.1.2]。仅图标的按钮需使用 aria-label,图标本身需使用 aria-hidden="true"。可见的标签文本必须是可访问名称的一部分 [2.5.3]。链接必须说明去向——不要使用“点击这里” [2.4.4]。
键盘支持 [2.1.1, 2.1.2]。所有能用指针操作的内容也必须能用键盘操作;不能有焦点陷阱。Tab 顺序应与视觉顺序一致;永远不要使用正的 tabIndex。Escape 键关闭最上层的覆盖层。拖拽和手势操作必须提供单指针替代方案 [2.5.7, 2.5.1]。
可见焦点 [2.4.7, 1.4.11]。为 :focus-visible 设置样式;永远不要使用 outline: none 而不提供经过验证的替代方案(与相邻颜色的对比度 ≥ 3:1)。焦点不应被固定头部/条遮挡 [2.4.11]——使用 scroll-padding-top。
焦点管理。 对话框:打开时焦点移入,焦点被限制在其中,背景设为 inert,关闭时焦点返回触发元素。SPA 路由变更:更新 document.title [2.4.2] 并将焦点移至新的 <h1>(设置 tabIndex={-1})或 <main>。
表单 [1.3.1, 3.3.1, 3.3.2]。每个控件都必须有可见的 <label>——占位符不是标签。使用正确的 type、inputMode 和 autoComplete [1.3.5]。错误信息需内联显示,使用 aria-describedby 关联,设置字段 aria-invalid="true",提交时将焦点聚焦到第一个无效字段。在提交时验证——不要禁用提交按钮直到表单有效。永远不要阻止粘贴 [3.3.8]。
宣布动态变更 [4.1.3]。字段特定错误 → 使用 aria-describedby;非紧急消息(已保存、结果数量、加载中)→ 使用 role="status";紧急表单级别错误 → 使用 role="alert"。实时区域必须在 DOM 中存在 在 其文本变更之前。
颜色与对比度 [1.4.3, 1.4.11, 1.4.1]。文本对比度 ≥ 4.5:1(大文本 ≥ 3:1),UI 边界、图标和焦点环对比度 ≥ 3:1——通过设计令牌强制执行。颜色永远不能是唯一的提示。
目标尺寸 [2.5.8]。≥ 24×24 CSS px 或有足够间距;桌面端建议 40 px / 触摸设备 44 px。装饰性覆盖层需设置 pointer-events: none。
动画与时间 [2.3.3, 2.2.2]。动画通过 @media (prefers-reduced-motion: no-preference) 选择启用;在减少动画偏好下,视差/自动播放需关闭。任何持续超过 5 秒的运动必须可暂停。包含操作或错误的通知必须保持显示直到用户关闭。
缩放与重排 [1.4.4, 1.4.10]。在 200% 文字大小和 320 CSS px 宽度下,内容不出现水平滚动条。绝不要使用 maximum-scale=1 / user-scalable=no。
页面基础。 设置 <html lang> 并与 UI 语言同步更新 [3.1.1];具有一个可见的 <main>;标题形成大纲;当导航区位于 <main> 之前时,第一个元素是跳转链接 [2.4.1]。
图片。 有意义的图片 → alt 描述目的;装饰性图片 → alt="";功能性图片(图片作为链接/按钮)→ alt 描述操作 [1.1.1]。
测试职责
每次 UI 变更必须包含:
- 组件测试,通过角色和名称查询(
getByRole),并覆盖键盘路径(user.tab()、{Escape}、toHaveFocus()) - Storybook 故事,其 addon-a11y 检查以 错误 级别运行(导致 CI 失败)
- 对关键页面和状态执行 Playwright +
@axe-core/playwright扫描(WCAG 2.x A/AA 标签) - 手动仅键盘操作流程;对关键流程进行屏幕阅读器(VoiceOver/NVDA)测试
自动化工具只能发现部分问题——干净的 axe 运行结果并不代表 WCAG 合规。详细信息请参考 references/testing.md。
完成前——扫描变更的常见模式
| 模式 | 修复方法 |
|---|---|
<div onClick / <span onClick | 使用 <button>,或者如果是导航则使用 <a href> |
role="button"/role="tab" 无键盘处理 | 使用原生元素或 APG 键映射 |
outline: none / outline-none 无 focus-visible 替代方案 | 保留浏览器默认焦点环或经过验证的自定义焦点环 |
正的 tabIndex | 调整 DOM 顺序,使用 0 / -1 |
<img 无 alt | 添加 alt="" 或目的描述 |
| 图标按钮无名称 | 添加 aria-label,图标设置 aria-hidden |
输入无 <label> / 占位符作为标签 | 添加可见且关联的标签 |
disabled={!isValid} 在提交按钮上 | 保持启用,在提交时验证 |
{msg && <div role="status">…} | 先渲染空的区域,然后更新其文本 |
成功消息上使用 aria-live="assertive" | 使用 role="status" |
在可聚焦元素或其父级上使用 aria-hidden | 移除它,或使子树变为 inert |
onPaste + preventDefault | 允许粘贴 |
maximum-scale=1 / user-scalable=no | 移除 |
固定头部无 scroll-padding-top | 为滚动容器添加内边距 |
| 拖拽/手势无按钮替代方案 | 添加单指针路径 |
transition: all,动画无减少运动保护 | 列出具体属性,包裹在 no-preference 中 |
报告
审计和审查应使用对话语言报告,每个根本原因一条发现,附带所有位置、WCAG 标准、用户影响以及具体修复方法——格式见 references/audit.md。严重程度映射到代码审查技能:HIGH → 阻塞/主要,MEDIUM → 主要/次要,LOW → 次要。
来源
综合自:Addy Osmani 的 web-quality-skills accessibility(MIT)、Jakub Krehel 的 better-accessibility(MIT)、ibelick 的 fixing-accessibility(MIT)、Vercel 的 Web Interface Guidelines(MIT);W3C WCAG 2.2 和 WAI-ARIA Authoring Practices。