可访问性(a11y)
让 Agent 在实现 UI 时同步检查焦点顺序、可见标签与动态内容的屏幕阅读器播报。
本页为 Agent 提供可访问性实施的完整参考:ARIA 角色与属性的正确用法、模态框焦点陷阱实现、语义 HTML 对比、jest-axe 自动测试配置,以及屏幕阅读器测试清单。目标级别为 WCAG 2.2 AA。
SKILL 写明必测流程:仅键盘操作(Tab/Shift+Tab/Enter/Space)、缩放 200%、prefers-reduced-motion;自动化 axe 与 VoiceOver/NVDA 人工走查分工明确。
ARIA 角色与键盘焦点陷阱
ARIA 属性正确用法:
<!-- ✅ 表单错误:aria-invalid + aria-describedby -->
<input
id="email"
type="email"
aria-invalid="true"
aria-describedby="email-error"
aria-required="true"
/>
<span id="email-error" role="alert">请输入有效的电子邮件地址</span>
<!-- ✅ 动态内容:aria-live -->
<div aria-live="polite" aria-atomic="true" class="sr-only">
<!-- 搜索结果数量更新时自动播报 -->
找到 {{ resultCount }} 条结果
</div>
<!-- ✅ 图标按钮:aria-label -->
<button type="button" aria-label="关闭对话框">
<svg aria-hidden="true" focusable="false">...</svg>
</button>
模态框焦点陷阱(Focus Trap)实现:
// utils/focusTrap.ts
export function createFocusTrap(container: HTMLElement) {
const focusable = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
function getFocusable() {
return Array.from(container.querySelectorAll<HTMLElement>(focusable))
.filter(el => !el.hasAttribute('disabled'))
}
function handleKeydown(e: KeyboardEvent) {
if (e.key !== 'Tab') return
const els = getFocusable()
if (!els.length) return
const first = els[0], last = els[els.length - 1]
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault()
last.focus()
}
} else {
if (document.activeElement === last) {
e.preventDefault()
first.focus()
}
}
}
return {
activate() {
container.addEventListener('keydown', handleKeydown)
getFocusable()[0]?.focus() // 自动聚焦第一个可交互元素
},
deactivate() {
container.removeEventListener('keydown', handleKeydown)
},
}
}
- 错误信息用
aria-describedby/aria-invalid与控件关联,而非仅靠颜色。 - 对话框关闭时将焦点还原到触发元素(
triggerEl.focus())。 - 自定义控件(如下拉菜单)保持 roving tabindex 模式时,在 SKILL 中写清交互契约。
语义 HTML 对比与 jest-axe 自动测试
语义 HTML 对比(div+onclick vs 正确元素):
<!-- ❌ 反模式:div 模拟按钮 -->
<div class="btn" onclick="submit()">提交</div>
<!-- 问题:无键盘访问、无 role、屏幕阅读器不识别 -->
<!-- ✅ 正确:使用原生 button -->
<button type="submit">提交</button>
<!-- 自动获得:键盘 Enter/Space、role="button"、focus 管理 -->
<!-- ❌ 反模式:span 模拟链接 -->
<span class="link" onclick="navigate('/about')">关于我们</span>
<!-- ✅ 正确:使用 anchor -->
<a href="/about">关于我们</a>
<!-- 支持右键在新标签打开、屏幕阅读器链接列表等 -->
jest-axe 自动化测试配置:
// jest.setup.ts
import { toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
// __tests__/Button.test.tsx
import { render } from '@testing-library/react'
import { axe } from 'jest-axe'
import { Button } from '@/components/Button'
test('Button 无可访问性违规', async () => {
const { container } = render(
<Button onClick={() => {}}>提交</Button>
)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
// package.json 配置
// "jest-axe": "^9.0.0"
// "@testing-library/jest-dom": "^6.0.0"
屏幕阅读器测试清单(必须用 VoiceOver/NVDA 验证的 5 个场景):
- 场景 1:仅用 Tab 键完成表单提交全流程(填写→验证→提交)。
- 场景 2:打开模态框→执行操作→关闭,焦点正确返回触发元素。
- 场景 3:动态内容更新(搜索结果/通知)被
aria-live正确播报。 - 场景 4:图片/图表有替代文本,装饰性图片用
alt=""静默。 - 场景 5:页面标题层级连续(H1→H2→H3),导航地标
main/nav/footer齐备。
Agent 走查流程
将「级别与范围 → 自动化 → 键盘与缩放 → 记录例外」写进 SKILL,便于按顺序执行。
[ SKILL:WCAG 版本 + 级别(如 2.2 AA)+ 范围(页面/组件) ]
│
▼
[ 自动化:axe / 同类规则;阈值与基线策略 ]
│
▼
[ 人工:仅键盘 Tab/Shift+Tab、Enter/Space;缩放 200% ]
│
▼
[ 动态:对话框/路由 — 焦点 trap 与返回;live 区域 ]
│
▼
[ 输出:问题清单 + 第三方无法修时的工单/替代方案 ]
页内工具:WCAG 验收要点摘要
以下脚本仅在本页运行;勾选后生成可复制进 SKILL 或评审备注的条目列表。
SKILL 与走查对齐
勾选已纳入 SKILL 或本次迭代要验收的项,点击生成摘要。
SKILL 大纲
---
name: accessibility-a11y-cn
description: 按 WCAG 2.2 AA 检查前端可访问性与键盘/读屏体验
---
# 规则
- 使用语义 HTML:button/a/input 替代 div+onclick
- 所有交互元素有可访问名称(可见文本、aria-label 或 aria-labelledby)
- 颜色对比度文本 ≥ 4.5:1,大文本 ≥ 3:1(使用 color-contrast-checker 验证)
- 动态内容更新:使用 aria-live="polite" 或 role="alert"
- 模态框:打开时焦点移入,关闭时还原触发元素,Tab 循环不逃出
# 步骤
1. 安装 jest-axe:npm i -D jest-axe @types/jest-axe
2. jest.setup.ts:import { toHaveNoViolations } from 'jest-axe'; expect.extend(toHaveNoViolations)
3. 每个组件测试加 axe(container) 断言(CI 阻断违规)
4. 键盘走查:Tab/Shift+Tab 遍历所有控件,Enter/Space 激活,Escape 关闭模态
5. VoiceOver(macOS):Cmd+F5 开启,VO+Right 逐元素;NVDA(Win):NVDA+空格 浏览
6. 缩放 200%:内容不溢出、不遮挡;prefers-reduced-motion:动画降级
7. 输出:违规清单按 WCAG 成功标准编号分组,附修复建议与优先级