mcpField
A
Accessibility Engineer

Accessibility Engineer

github.com/daffy0208设计与 UI38177 次安装44 周前更新
代码仓库

关于 Accessibility Engineer

面向设计与开发工作的无障碍工程技能指南,以 WCAG 2.1 AA 为目标。内容覆盖语义 HTML、键盘操作、ARIA、表单、对比度和测试。

主要功能

  1. 01语义 HTML 与页面结构
  2. 02键盘导航和焦点管理
  3. 03ARIA 属性使用规范
  4. 04表单标签与错误提示
  5. 05颜色对比及图像替代文本
  6. 06自动化与手动无障碍测试

适用场景

  1. 01检查页面是否支持键盘操作
  2. 02为表单补充标签和关联错误
  3. 03审查颜色对比与图像替代文本
  4. 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