Accessibility Engineer by daffy0208/ai-dev-standards
npx skills add https://github.com/daffy0208/ai-dev-standards --skill 'Accessibility Engineer'为所有人构建——无障碍不是可选项。
无障碍是一项公民权利,而非功能特性。
在美国,每四位成年人中就有一位患有残疾。无障碍设计惠及所有人:
A 级: 最低要求(法律要求) AA 级: 行业标准(以此为目标) AAA 级: 黄金标准(对所有内容都难以实现)
目标:符合 WCAG 2.1 AA 标准
// ❌ 不佳:所有内容都用 div(无语义含义)
<div onClick={handleClick}>Click me</div>
<div>Menu</div>
// ✅ 良好:语义化 HTML
<button onClick={handleClick}>Click me</button>
<nav>Menu</nav>
// ✅ 正确的标题层级
<h1>Page Title</h1>
<h2>Section 1</h2>
<h3>Subsection 1.1</h3>
<h2>Section 2</h2>
// ❌ 不佳:跳过层级
<h1>Page Title</h1>
<h4>Section 1</h4> // 跳过了 h2, h3
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
<header>
<nav aria-label="Main navigation">
{/* Navigation links */}
</nav>
</header>
<main>
<article>
{/* Main content */}
</article>
<aside>
{/* Sidebar */}
</aside>
</main>
<footer>
{/* Footer content */}
</footer>
// ✅ 按钮默认支持键盘访问
<button onClick={handleClick}>Click me</button>
// ❌ Div 需要额外工作
<div onClick={handleClick}>Click me</div> // 无法通过 Tab 键聚焦!
// ✅ 如果必须使用 div,请添加键盘支持
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick()
}
}}
>
Click me
</div>
// ✅ 自然的 Tab 键顺序(遵循 DOM 顺序)
<input />
<button>Submit</button>
<a href="/help">Help</a>
// ❌ 不要使用 tabIndex > 0(破坏自然顺序)
<button tabIndex={5}>Button</button> // 反模式!
// ✅ tabIndex=-1 以从 Tab 键顺序中移除
<div tabIndex={-1}>Not keyboard focusable</div>
// 模态框:将焦点限制在内部
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef()
useEffect(() => {
if (!isOpen) return
// 聚焦第一个可聚焦元素
const firstFocusable = modalRef.current.querySelector('button, input, a')
firstFocusable?.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
}
// 允许键盘用户跳过导航
<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;
}
// ✅ 语义化 HTML(无需 ARIA)
<button>Click me</button>
// ❌ 不必要的 ARIA
<button role="button" aria-label="Click me">Click me</button>
// ✅ 需要 ARIA(自定义组件)
<div role="tab" aria-selected={isActive} aria-controls="panel-1">
Tab 1
</div>
aria-label - 提供无障碍名称:
<button aria-label="Close dialog">
<XIcon /> {/* 仅视觉可见 */}
</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:等待用户完成操作
<div aria-live="polite">
{itemsAddedToCart} items added to cart
</div>
// Assertive:立即中断(谨慎使用)
<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 - 对屏幕阅读器隐藏:
// 装饰性图标
<span aria-hidden="true">★</span>
// 不要隐藏交互元素!
// ❌ 不佳
<button aria-hidden="true">Click me</button>
// ✅ 良好:显式标签
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// ✅ 良好:隐式标签
<label>
Email
<input type="email" />
</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" />
// ❌ 不佳:对比度不足
<button style={{ background: '#ddd', color: '#aaa' }}>
Submit // 1.5:1 对比度 - 失败!
</button>
// ✅ 良好:对比度足够
<button style={{ background: '#0066cc', color: '#ffffff' }}>
Submit // 8:1 对比度 - 通过!
</button>
// ❌ 不佳:仅使用颜色
<span style={{ color: 'red' }}>Error</span>
<span style={{ color: 'green' }}>Success</span>
// ✅ 良好:颜色 + 图标/文本
<span style={{ color: 'red' }}>
<ErrorIcon aria-hidden="true" />
Error
</span>
// ✅ 信息性图像
<img src="chart.png" alt="Sales increased 50% in Q4" />
// ✅ 装饰性图像
<img src="decorative-border.png" alt="" /> // 空 alt
// ❌ 不佳:无 alt 或冗余 alt
<img src="photo.jpg" /> // 缺少 alt
<img src="photo.jpg" alt="Photo" /> // 无意义
<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>
# Lighthouse 无障碍审计
lighthouse https://example.com --only-categories=accessibility
# axe-core (Jest)
npm install --save-dev @axe-core/react jest-axe
// 使用 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()
})
键盘导航:
屏幕阅读器测试:
屏幕阅读器快捷键:
<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>
技能:
ux-designer - 无障碍设计模式frontend-builder - 无障碍 React 组件testing-strategist - 无障碍测试外部资源:
为所有人构建。 ♿
每周安装数
0
代码仓库
GitHub 星标数
18
首次出现
1970年1月1日
安全审计
Build for everyone - accessibility is not optional.
Accessibility is a civil right, not a feature.
1 in 4 adults in the US has a disability. Accessible design benefits everyone:
Level A: Minimum (legal requirement) Level AA: Industry standard (aim for this) Level AAA: Gold standard (difficult to achieve for all content)
Target: WCAG 2.1 AA compliance
// ❌ 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>
// ✅ 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>
// ✅ 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;
}
// ✅ 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-label - Provides accessible name:
<button aria-label="Close dialog">
<XIcon /> {/* Visual only */}
</button>
<input type="search" aria-label="Search products" />
aria-labelledby - References another element:
<h2 id="dialog-title">Delete Account</h2>
<div role="dialog" aria-labelledby="dialog-title">
{/* Dialog content */}
</div>
aria-describedby - Additional description:
<input
type="password"
aria-describedby="password-requirements"
/>
<div id="password-requirements">
Must be at least 8 characters
</div>
aria-live - Announce dynamic content:
// 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 - Collapsible content:
<button
aria-expanded={isOpen}
aria-controls="dropdown-menu"
onClick={() => setIsOpen(!isOpen)}
>
Menu
</button>
<div id="dropdown-menu" hidden={!isOpen}>
{/* Menu items */}
</div>
aria-hidden - Hide from screen readers:
// Decorative icons
<span aria-hidden="true">★</span>
// Don't hide interactive elements!
// ❌ Bad
<button aria-hidden="true">Click me</button>
// ✅ 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" />
Normal text (< 18pt): 4.5:1
Large text (≥ 18pt or 14pt bold): 3:1
UI components: 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>
// ✅ 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>
# 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()
})
Keyboard Navigation:
Screen Reader Testing:
Screen Reader Shortcuts:
<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>
Skills:
ux-designer - Accessible design patternsfrontend-builder - Accessible React componentstesting-strategist - Accessibility testingExternal:
Build for everyone. ♿
Weekly Installs
0
Repository
GitHub Stars
18
First Seen
Jan 1, 1970
Security Audits
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
111,800 周安装
SQL性能优化助手 - 专业SQL查询调优与索引策略优化工具
8,600 周安装
Vue Router 最佳实践指南:导航守卫、路由生命周期与常见陷阱解决方案
9,200 周安装
后台代理状态通知 - 避免轮询浪费,高效处理AI代理进度更新
205 周安装
网站设计审查工具 - 自动检测并修复HTML/CSS/JS、React、Vue等框架的视觉与布局问题
8,900 周安装
.NET/C# 最佳实践指南:代码规范、设计模式、依赖注入与AI集成
8,900 周安装
Playwright MCP 测试生成工具 - 自动生成 TypeScript 端到端测试代码
9,000 周安装