关于 Accessibility Engineer
面向设计与开发工作的无障碍工程技能指南,以 WCAG 2.1 AA 为目标。内容覆盖语义 HTML、键盘操作、ARIA、表单、对比度和测试。
主要功能
- 01语义 HTML 与页面结构
- 02键盘导航和焦点管理
- 03ARIA 属性使用规范
- 04表单标签与错误提示
- 05颜色对比及图像替代文本
- 06自动化与手动无障碍测试
适用场景
- 01检查页面是否支持键盘操作
- 02为表单补充标签和关联错误
- 03审查颜色对比与图像替代文本
- 04运行 axe 或 Lighthouse 无障碍检查
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ daffy0208/ ai-dev-standards --skill accessibility-engineer安装后在 Claude Code 中输入/accessibility-engineer确认已加载。
附带文件README.mdmanifest.yaml
详细介绍
无障碍工程师
为每个人打造产品——无障碍不是可选项。
核心原则
无障碍是一项公民权利,而不是一项功能。
美国每 4 位成年人中就有 1 位残障人士。无障碍设计惠及所有人:
- 盲人用户(屏幕阅读器)
- 低视力用户(缩放、高对比度)
- 失聪用户(字幕)
- 有运动障碍的用户(仅使用键盘)
- 有认知障碍的用户(清晰的语言)
- 暂时有障碍的用户(手臂受伤)
- 受环境条件限制的用户(强烈阳光、嘈杂环境)
WCAG 合规等级
A 级: 最低标准(法律要求) AA 级: 行业标准(以此为目标) AAA 级: 黄金标准(难以适用于所有内容)
目标:符合 WCAG 2.1 AA 标准
支柱 1:语义化 HTML
使用恰当的元素
// ❌ Bad: Divs for everything (no semantic meaning)
<div onClick={handleClick}>Click me</div>
<div>Menu</div>
// ✅ Good: Semantic HTML
<button onClick={handleClick}>Click me</button>
<nav>Menu</nav>
文档结构
// ✅ Proper heading hierarchy
<h1>Page Title</h1>
<h2>Section 1</h2>
<h3>Subsection 1.1</h3>
<h2>Section 2</h2>
// ❌ Bad: Skipping levels
<h1>Page Title</h1>
<h4>Section 1</h4> // Skipped h2, h3
地标
<header>
<nav aria-label="Main navigation">
{/* Navigation links */}
</nav>
</header>
<main>
<article>
{/* Main content */}
</article>
<aside>
{/* Sidebar */}
</aside>
</main>
<footer>
{/* Footer content */}
</footer>
支柱 2:键盘导航
所有交互元素都必须支持键盘访问
// ✅ Button is keyboard accessible by default
<button onClick={handleClick}>Click me</button>
// ❌ Div requires extra work
<div onClick={handleClick}>Click me</div> // Can't tab to it!
// ✅ If you must use div, add keyboard support
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick()
}
}}
>
Click me
</div>
Tab 顺序
// ✅ Natural tab order (follows DOM order)
<input />
<button>Submit</button>
<a href="/help">Help</a>
// ❌ Don't use tabIndex > 0 (breaks natural order)
<button tabIndex={5}>Button</button> // Anti-pattern!
// ✅ tabIndex=-1 to remove from tab order
<div tabIndex={-1}>Not keyboard focusable</div>
焦点管理
// Modal: Trap focus inside
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef()
useEffect(() => {
if (!isOpen) return
// Focus first focusable element
const firstFocusable = modalRef.current.querySelector('button, input, a')
firstFocusable?.focus()
// Trap focus
function handleTab(e) {
if (e.key !== 'Tab') return
const focusableElements = modalRef.current.querySelectorAll(
'button, input, a, [tabindex]:not([tabindex="-1"])'
)
const first = focusableElements[0]
const last = focusableElements[focusableElements.length - 1]
if (e.shiftKey) {
if (document.activeElement === first) {
last.focus()
e.preventDefault()
}
} else {
if (document.activeElement === last) {
first.focus()
e.preventDefault()
}
}
}
document.addEventListener('keydown', handleTab)
return () => document.removeEventListener('keydown', handleTab)
}, [isOpen])
return isOpen ? (
<div role="dialog" aria-modal="true" ref={modalRef}>
{children}
<button onClick={onClose}>Close</button>
</div>
) : null
}
跳转链接
// Allow keyboard users to skip navigation
<a href="#main-content" className="skip-link">
Skip to main content
</a>
<nav>{/* Navigation */}</nav>
<main id="main-content">
{/* Main content */}
</main>
// CSS
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
}
.skip-link:focus {
top: 0;
}
支柱 3:ARIA 属性
仅在语义化 HTML 不足以满足需求时使用 ARIA
// ✅ Semantic HTML (no ARIA needed)
<button>Click me</button>
// ❌ Unnecessary ARIA
<button role="button" aria-label="Click me">Click me</button>
// ✅ ARIA needed (custom widget)
<div role="tab" aria-selected={isActive} aria-controls="panel-1">
Tab 1
</div>
常见 ARIA 属性
aria-label - 提供无障碍名称:
<button aria-label="Close dialog">
<XIcon /> {/* Visual only */}
</button>
<input type="search" aria-label="Search products" />
aria-labelledby - 引用另一个元素:
<h2 id="dialog-title">Delete Account</h2>
<div role="dialog" aria-labelledby="dialog-title">
{/* Dialog content */}
</div>
aria-describedby - 提供额外说明:
<input
type="password"
aria-describedby="password-requirements"
/>
<div id="password-requirements">
Must be at least 8 characters
</div>
aria-live - 播报动态内容:
// Polite: Wait for user to finish
<div aria-live="polite">
{itemsAddedToCart} items added to cart
</div>
// Assertive: Interrupt immediately (use sparingly)
<div aria-live="assertive" role="alert">
Error: Payment failed
</div>
aria-expanded - 表示可折叠内容:
<button
aria-expanded={isOpen}
aria-controls="dropdown-menu"
onClick={() => setIsOpen(!isOpen)}
>
Menu
</button>
<div id="dropdown-menu" hidden={!isOpen}>
{/* Menu items */}
</div>
aria-hidden - 对屏幕阅读器隐藏内容:
// Decorative icons
<span aria-hidden="true">★</span>
// Don't hide interactive elements!
// ❌ Bad
<button aria-hidden="true">Click me</button>
支柱 4:表单和输入项
标签
// ✅ Good: Explicit label
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// ✅ Good: Implicit label
<label>
Email
<input type="email" />
</label>
// ❌ Bad: No label (placeholder is not a label!)
<input type="email" placeholder="Email" />
错误消息
function EmailInput({ error }) {
const errorId = 'email-error'
return (
<>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-invalid={!!error}
aria-describedby={error ? errorId : undefined}
/>
{error && (
<div id={errorId} role="alert">
{error}
</div>
)}
</>
)
}
必填字段
<label htmlFor="name">
Name <span aria-label="required">*</span>
</label>
<input id="name" type="text" required aria-required="true" />
支柱 5:颜色和对比度
最低对比度比值(WCAG AA)
- 普通文本(< 18pt):4.5:1
- 大号文本(≥ 18pt 或 14pt 粗体):3:1
- UI 组件:3:1
// ❌ Bad: Insufficient contrast
<button style={{ background: '#ddd', color: '#aaa' }}>
Submit // 1.5:1 contrast - fails!
</button>
// ✅ Good: Sufficient contrast
<button style={{ background: '#0066cc', color: '#ffffff' }}>
Submit // 8:1 contrast - passes!
</button>
不要仅依靠颜色传达信息
// ❌ Bad: Color only
<span style={{ color: 'red' }}>Error</span>
<span style={{ color: 'green' }}>Success</span>
// ✅ Good: Color + icon/text
<span style={{ color: 'red' }}>
<ErrorIcon aria-hidden="true" />
Error
</span>
支柱 6:图像和媒体
替代文本
// ✅ Informative images
<img src="chart.png" alt="Sales increased 50% in Q4" />
// ✅ Decorative images
<img src="decorative-border.png" alt="" /> // Empty alt
// ❌ Bad: No alt or redundant alt
<img src="photo.jpg" /> // Missing alt
<img src="photo.jpg" alt="Photo" /> // Useless
复杂图像
<figure>
<img src="complex-chart.png" alt="Sales data for 2024" />
<figcaption>
<details>
<summary>Detailed description</summary>
<p>Q1: $100k, Q2: $150k, Q3: $180k, Q4: $220k. Shows 50% growth year-over-year.</p>
</details>
</figcaption>
</figure>
视频字幕
<video controls>
<source src="video.mp4" type="video/mp4" />
<track kind="captions" src="captions.vtt" srclang="en" label="English" default />
</video>
支柱 7:测试
自动化测试
# Lighthouse accessibility audit
lighthouse https://example.com --only-categories=accessibility
# axe-core (Jest)
npm install --save-dev @axe-core/react jest-axe
// Test with jest-axe
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
it('has no accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
手动测试
键盘导航:
- 使用 Tab 键遍历整个页面
- 使用 Enter/Space 键激活按钮
- 在单选按钮组中使用方向键
- 使用 Esc 键关闭模态框
屏幕阅读器测试:
- NVDA(Windows,免费)
- JAWS(Windows,付费)
- VoiceOver(Mac,内置)
屏幕阅读器快捷键:
- 按标题导航:H(下一个),Shift+H(上一个)
- 按地标导航:D(下一个),Shift+D(上一个)
- 列出所有链接:Insert+F7(NVDA)
常见模式
无障碍按钮
<button
type="button"
onClick={handleClick}
disabled={isDisabled}
aria-busy={isLoading}
aria-label={ariaLabel}
>
{children}
</button>
无障碍模态框
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Dialog Title</h2>
<p id="dialog-description">Dialog description</p>
<button onClick={onClose}>Close</button>
</div>
无障碍标签页
<div>
<div role="tablist">
<button
role="tab"
aria-selected={activeTab === 'tab1'}
aria-controls="panel1"
onClick={() => setActiveTab('tab1')}
>
Tab 1
</button>
<button
role="tab"
aria-selected={activeTab === 'tab2'}
aria-controls="panel2"
onClick={() => setActiveTab('tab2')}
>
Tab 2
</button>
</div>
<div id="panel1" role="tabpanel" hidden={activeTab !== 'tab1'}>
Panel 1 content
</div>
<div id="panel2" role="tabpanel" hidden={activeTab !== 'tab2'}>
Panel 2 content
</div>
</div>
无障碍检查清单
语义化 HTML
- 标题层级正确(h1 → h2 → h3)
- 使用语义化地标(header、nav、main、footer)
- 列表使用 ul/ol/li
- 操作使用按钮,导航使用链接
键盘
- 所有交互元素都支持键盘访问
- 焦点指示器清晰可见
- Tab 顺序合理
- 提供跳转链接
- 没有键盘陷阱
ARIA
- 优先使用语义化 HTML(仅在必要时使用 ARIA)
- 所有交互控件都设置了角色
- 动态内容使用 aria-live
- 表单具有恰当的标签和说明
颜色和对比度
- 文本对比度 ≥ 4.5:1(普通文本),≥ 3:1(大号文本)
- 不要仅依靠颜色传达信息
- 焦点指示器清晰可见
图像和媒体
- 所有图像都有替代文本
- 装饰性图像使用空替代文本
- 视频配有字幕
- 音频配有文字稿
表单
- 所有输入项都有标签
- 错误消息与对应输入项相关联
- 标明必填字段
测试
- 已测试键盘导航
- 已使用屏幕阅读器测试
- 已运行自动化工具(axe、Lighthouse)
- 已测试色盲模拟效果
工具
- axe DevTools - 浏览器扩展
- Lighthouse - 内置于 Chrome DevTools
- WAVE - Web 无障碍评估工具
- Color Contrast Analyzer - 桌面应用
- NVDA - 免费屏幕阅读器(Windows)
- VoiceOver - 内置屏幕阅读器(Mac)
相关资源
技能:
ux-designer- 无障碍设计模式frontend-builder- 无障碍 React 组件testing-strategist- 无障碍测试
外部资源:
为所有人构建。 ♿
评论
支持 Markdown