无障碍审计报告 (A11y Audit)
审计日期:2026-06-17
审计范围:src/shared/ 核心组件与新增无障碍工具
合规目标:WCAG 2.1 AA
一、已审计组件与 ARIA 改进
1. 新增无障碍工具库
| 文件 |
导出 |
用途 |
src/shared/lib/a11y.ts |
useA11yId |
基于 React.useId 生成 SSR 安全的唯一 ID,用于 aria-describedby、aria-labelledby |
src/shared/lib/a11y.ts |
mergeA11yProps |
合并多组 aria/data 属性,aria-*/data-* 字符串属性以空格拼接 |
src/shared/lib/a11y.ts |
describeInput |
计算输入框的 aria-describedby 与 aria-invalid |
src/shared/lib/a11y.ts |
loadingAria |
提供加载状态的 aria-busy 与 aria-live 属性 |
2. 新增 Hook
| 文件 |
导出 |
用途 |
src/shared/hooks/use-aria-live.ts |
useAriaLive |
管理 aria-live 区域,支持 polite/assertive 通知,自动清除过期通知(默认 5s),返回 { announce, liveRegion } |
3. 新增 a11y 组件
| 文件 |
组件 |
用途 |
src/shared/components/a11y/skip-link.tsx |
SkipLink |
跳转链接,视觉隐藏,获得焦点时高对比度显示,默认跳转 #main-content |
src/shared/components/a11y/visually-hidden.tsx |
VisuallyHidden |
视觉隐藏但屏幕阅读器可读,用于图标按钮文字描述、表单辅助说明 |
src/shared/components/a11y/focus-trap.tsx |
FocusTrap |
焦点陷阱,捕获 Tab/Shift+Tab 循环,支持初始焦点与焦点恢复 |
src/shared/components/a11y/aria-status.tsx |
AriaStatus |
ARIA 状态通知区域,渲染 aria-live 区域,支持 polite/assertive |
4. 增强的核心 UI 组件
src/shared/components/ui/table.tsx
| 组件 |
ARIA 改进 |
Table |
默认 role="table"(可覆盖),支持 aria-rowcount、aria-colcount |
TableHeader |
默认 role="rowgroup" |
TableBody |
默认 role="rowgroup" |
TableFooter |
默认 role="rowgroup" |
TableRow |
默认 role="row" |
TableHead |
默认 role="columnheader",支持 scope 属性(col/row/colgroup/rowgroup) |
TableCell |
默认 role="cell" |
TableCaption |
已有 <caption> 元素,为表格提供可访问标题 |
所有 role 均为默认值,可通过 props 覆盖,完全向后兼容。
src/shared/components/ui/dialog.tsx
| 改进项 |
说明 |
aria-modal="true" |
显式添加到 DialogContent(Radix 已内置,此处显式标注便于审计) |
关闭按钮 aria-label="关闭" |
添加明确的中文无障碍标签 |
| 关闭按钮 sr-only 文本 |
由 "Close" 改为 "关闭",与项目语言一致 |
| 焦点管理 |
Radix Dialog 原语已内置:打开时焦点移入内容区,关闭时恢复到触发元素 |
| Esc 键关闭 |
Radix Dialog 原语已内置 |
aria-labelledby |
Radix 自动关联 DialogTitle 的 id 到 aria-labelledby |
二、待改进项
| 优先级 |
项目 |
说明 |
| 高 |
表单组件 aria-describedby 关联 |
Input、Textarea、Select 等需配合 describeInput 工具函数,将错误提示和帮助文本的 id 关联到输入框 |
| 高 |
图标按钮 aria-label |
全项目排查仅含图标无文字的按钮,补充 aria-label 或使用 VisuallyHidden |
| 中 |
Sheet/AlertDialog 焦点管理 |
参照 Dialog 增强,显式添加 aria-modal 和中文关闭标签 |
| 中 |
数据表格 aria-rowcount/aria-colcount |
在使用 @tanstack/react-table 的页面中,为 Table 传入总行数和列数 |
| 中 |
面包屑 aria-label="面包屑导航" |
Breadcrumb 容器添加 nav 的 aria-label |
| 中 |
分页组件 aria-label |
分页导航添加 aria-label="分页",当前页使用 aria-current="page" |
| 低 |
动态内容变更播报 |
在表单提交、数据加载场景接入 useAriaLive 进行状态播报 |
| 低 |
颜色对比度审查 |
使用 axe DevTools 全量扫描颜色对比度是否达到 4.5:1(正文)/ 3:1(大文字) |
| 低 |
跳转链接全局应用 |
将 app/(dashboard)/layout.tsx 中的内联 skip-link 替换为 SkipLink 组件 |
三、屏幕阅读器测试指南
NVDA(Windows,免费)
- 安装:从 nvaccess.org 下载安装
- 启动/退出:
Ctrl + Alt + N 启动,Insert + Q 退出
- 核心快捷键:
↓ / ↑:逐行阅读
Tab / Shift + Tab:在可聚焦元素间移动
H:按标题跳转
T:跳转到表格
F:跳转到表单控件
B:跳转到按钮
Insert + Tab:播报当前焦点元素
Insert + Space:切换浏览/焦点模式
- 测试要点:
- 打开页面后 Tab 到 SkipLink,确认可跳转到主内容区
- Tab 遍历所有交互元素,确认每个元素有可读的名称
- 打开 Dialog,确认焦点移入对话框、Esc 可关闭、关闭后焦点回到触发按钮
- 在表格中按
T 跳转,确认表格标题和行列关系正确播报
VoiceOver(macOS,内置)
- 启动/退出:
Cmd + F5
- 核心快捷键:
Ctrl + Option + → / ←:逐元素导航
Ctrl + Option + Cmd + H:按标题跳转
Ctrl + Option + Cmd + T:跳转到表格
Ctrl + Option + Space:激活当前元素
Ctrl + Option + U:打开转子(Rotor)按元素类型浏览
- 测试要点:
- 确认 SkipLink 获得焦点时高对比度显示
- 确认
aria-live 区域在表单提交后播报结果
- 确认
VisuallyHidden 内容被播报但不可见
- 确认 Dialog 打开时 VoiceOver 朗读对话框标题
通用测试清单
四、WCAG 2.1 AA 合规检查清单
原则一:可感知 (Perceivable)
| 准则 |
状态 |
说明 |
| 1.1.1 非文本内容 |
✅ |
图标按钮通过 aria-label 或 VisuallyHidden 提供文字替代 |
| 1.2.1 纯音频/视频 |
⚠️ |
项目暂无音视频内容,后续如需添加需提供字幕/文字稿 |
| 1.3.1 信息与关系 |
✅ |
表格通过 role 和 scope 表达行列关系;表单通过 aria-describedby 关联说明 |
| 1.3.2 有意义的顺序 |
✅ |
DOM 顺序与视觉顺序一致 |
| 1.3.3 感官特征 |
✅ |
不仅依赖颜色/位置传达信息,配合文字说明 |
| 1.3.4 方向 |
✅ |
不限制屏幕方向 |
| 1.4.1 颜色的使用 |
✅ |
错误状态除颜色外配合文字/图标 |
| 1.4.3 对比度(最低) |
⚠️ |
需全量审查,语义色 muted-foreground 需确认对比度 ≥ 4.5:1 |
| 1.4.4 文字缩放 |
✅ |
使用 rem/em 单位,支持 200% 缩放 |
| 1.4.10 回流 |
✅ |
响应式布局,支持 320px 宽度 |
| 1.4.11 非文字对比度 |
✅ |
边框、焦点环使用语义色,对比度 ≥ 3:1 |
原则二:可操作 (Operable)
| 准则 |
状态 |
说明 |
| 2.1.1 键盘 |
✅ |
所有交互可通过键盘操作 |
| 2.1.2 无键盘陷阱 |
✅ |
FocusTrap 仅在模态框激活时使用,Esc 可退出 |
| 2.1.4 字符快捷键 |
✅ |
无单字符快捷键 |
| 2.2.1 计时可调 |
✅ |
无超时限制(会话超时由 NextAuth 管理,可延长) |
| 2.3.1 三次闪烁 |
✅ |
无闪烁内容 |
| 2.4.1 跳过区块 |
✅ |
SkipLink 组件提供跳转到主内容 |
| 2.4.2 页面标题 |
✅ |
Next.js metadata 提供页面标题 |
| 2.4.3 焦点顺序 |
✅ |
DOM 顺序符合逻辑 |
| 2.4.4 链接目的 |
✅ |
链接文字描述目的,避免"点击这里" |
| 2.4.6 标题与标签 |
✅ |
表单字段使用 Label 组件关联 |
| 2.4.7 焦点可见 |
✅ |
所有交互元素有 focus:ring 样式 |
| 2.5.3 标签包含名称 |
✅ |
可见标签文字包含在可访问名称中 |
原则三:可理解 (Understandable)
| 准则 |
状态 |
说明 |
| 3.1.1 页面语言 |
✅ |
<html lang="zh-CN"> |
| 3.1.2 部分语言 |
✅ |
暂无混语言内容 |
| 3.2.1 聚焦 |
✅ |
聚焦不触发意外上下文变更 |
| 3.2.2 输入 |
✅ |
表单提交需明确按钮触发 |
| 3.2.3 一致导航 |
✅ |
侧边栏导航在页面间一致 |
| 3.2.4 一致标识 |
✅ |
功能相同的组件使用一致标识 |
| 3.3.1 错误识别 |
✅ |
表单错误通过 aria-invalid 和 aria-describedby 播报 |
| 3.3.2 标签或说明 |
✅ |
表单字段使用 Label 关联,提供 placeholder 补充 |
| 3.3.3 错误建议 |
⚠️ |
部分表单错误仅提示"必填",需补充修正建议 |
| 3.3.4 错误预防 |
✅ |
删除/提交关键操作使用 AlertDialog 确认 |
原则四:健壮 (Robust)
| 准则 |
状态 |
说明 |
| 4.1.1 解析 |
✅ |
React 保证有效 HTML |
| 4.1.2 名称、角色、值 |
✅ |
ARIA 角色和属性正确设置,状态变化通过 aria-live 播报 |
| 4.1.3 状态消息 |
✅ |
useAriaLive 和 AriaStatus 提供 aria-live 状态播报 |
五、自动化测试工具推荐
六、使用示例
useAriaLive — 表单提交结果播报
describeInput — 输入框错误关联
FocusTrap — 自定义模态框