mcpField
A
accessibility

accessibility

github.com/tech-leads-club设计与 UI7.1k243 次安装今天更新
代码仓库

关于 accessibility

Accessibility 是一项参考技能,帮助将 WCAG 2.1 和 Lighthouse 无障碍指南用于数字内容与界面。内容按 POUR 原则组织,并包含实现示例及自动化、手动测试方法。

主要功能

  1. 01按 POUR 原则整理 WCAG 2.1 指南
  2. 02提供无障碍界面模式示例
  3. 03涵盖色彩对比度与键盘操作
  4. 04提供自动化审计命令示例
  5. 05提供手动无障碍测试清单

适用场景

  1. 01检查页面的键盘访问与焦点状态
  2. 02核查图像替代文本和表单标签
  3. 03在发布前准备无障碍检查

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/tech-leads-club/agent-skills --skill accessibility

安装后在 Claude Code 中输入/accessibility确认已加载。

附带文件references/WCAG.md

详细介绍

无障碍访问(a11y)

基于 WCAG 2.1 和 Lighthouse 无障碍审计的全面无障碍指南。目标:让所有人都能使用内容,包括残障人士。

WCAG 原则:POUR

原则说明
Perceivable内容可通过不同感官感知
Operable所有用户都能操作界面
Understandable内容和界面易于理解
Robust内容可与辅助技术配合使用

合规等级

等级要求目标
A最低无障碍要求必须通过
AA标准合规应通过(许多司法管辖区的法律要求)
AAA增强无障碍要求最好达到

可感知性

文本替代(1.1)

图片需要替代文本:


<img src="chart.png" />


<img src="chart.png" alt="Bar chart showing 40% increase in Q3 sales" />


<img src="decorative-border.png" alt="" role="presentation" />


<figure>
  <img src="infographic.png" alt="2024 market trends infographic" aria-describedby="infographic-desc" />
  <figcaption id="infographic-desc">
    
  </figcaption>
</figure>

图标按钮需要无障碍名称:


<button>
  <svg></svg>
</button>


<button aria-label="Open menu">
  <svg aria-hidden="true"></svg>
</button>


<button>
  <svg aria-hidden="true"></svg>
  <span class="visually-hidden">Open menu</span>
</button>

视觉隐藏类:

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
色彩对比度(1.4.3、1.4.6)
文本大小AA 最低要求AAA 增强要求
普通文本(< 18px / < 14px 粗体)4.5:17:1
大号文本(≥ 18px / ≥ 14px 粗体)3:14.5:1
UI 组件和图形3:13:1
/* ❌ Low contrast (2.5:1) */
.low-contrast {
  color: #999;
  background: #fff;
}

/* ✅ Sufficient contrast (7:1) */
.high-contrast {
  color: #333;
  background: #fff;
}

/* ✅ Focus states need contrast too */
:focus-visible {
  outline: 2px solid #005fcc;
  outline-offset: 2px;
}

不要仅依靠颜色传达信息:


<input class="error-border" />
<style>
  .error-border {
    border-color: red;
  }
</style>


<div class="field-error">
  <input aria-invalid="true" aria-describedby="email-error" />
  <span id="email-error" class="error-message">
    <svg aria-hidden="true"></svg>
    Please enter a valid email address
  </span>
</div>
媒体替代内容(1.2)

<video controls>
  <source src="video.mp4" type="video/mp4" />
  <track kind="captions" src="captions.vtt" srclang="en" label="English" default />
  <track kind="descriptions" src="descriptions.vtt" srclang="en" label="Descriptions" />
</video>


<audio controls>
  <source src="podcast.mp3" type="audio/mp3" />
</audio>
<details>
  <summary>Transcript</summary>
  <p>Full transcript text...</p>
</details>

可操作性

键盘可访问(2.1)

所有功能都必须支持键盘操作:

// ❌ Only handles click
element.addEventListener('click', handleAction)

// ✅ Handles both click and keyboard
element.addEventListener('click', handleAction)
element.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault()
    handleAction()
  }
})

不得出现键盘陷阱:

// Modal focus management
function openModal(modal) {
  const focusableElements = modal.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
  )
  const firstElement = focusableElements[0]
  const lastElement = focusableElements[focusableElements.length - 1]

  // Trap focus within modal
  modal.addEventListener('keydown', (e) => {
    if (e.key === 'Tab') {
      if (e.shiftKey && document.activeElement === firstElement) {
        e.preventDefault()
        lastElement.focus()
      } else if (!e.shiftKey && document.activeElement === lastElement) {
        e.preventDefault()
        firstElement.focus()
      }
    }
    if (e.key === 'Escape') {
      closeModal()
    }
  })

  firstElement.focus()
}
焦点可见(2.4.7)
/* ❌ Never remove focus outlines */
*:focus {
  outline: none;
}

/* ✅ Use :focus-visible for keyboard-only focus */
:focus {
  outline: none;
}

:focus-visible {
  outline: 2px solid #005fcc;
  outline-offset: 2px;
}

/* ✅ Or custom focus styles */
button:focus-visible {
  box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5);
}
跳过链接(2.4.1)
<body>
  <a href="#main-content" class="skip-link">Skip to main content</a>
  <header></header>
  <main id="main-content" tabindex="-1">
    
  </main>
</body>
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #000;
  color: #fff;
  padding: 8px 16px;
  z-index: 100;
}

.skip-link:focus {
  top: 0;
}
时间限制(2.2)
// Allow users to extend time limits
function showSessionWarning() {
  const modal = createModal({
    title: 'Session Expiring',
    content: 'Your session will expire in 2 minutes.',
    actions: [
      { label: 'Extend session', action: extendSession },
      { label: 'Log out', action: logout },
    ],
    timeout: 120000, // 2 minutes to respond
  })
}
动效(2.3)
/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

易于理解

页面语言(3.1.1)

<html>
  
  <html lang="en">
    
    <p>The French word for hello is <span lang="fr">bonjour</span>.</p>
  </html>
</html>
导航一致性(3.2.3)

<nav aria-label="Main">
  <ul>
    <li><a href="/" aria-current="page">Home</a></li>
    <li><a href="/products">Products</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>
表单标签(3.3.2)

<input type="email" placeholder="Email" />


<label for="email">Email address</label>
<input type="email" id="email" name="email" autocomplete="email" required />


<label>
  Email address
  <input type="email" name="email" autocomplete="email" required />
</label>


<label for="password">Password</label>
<input type="password" id="password" aria-describedby="password-requirements" />
<p id="password-requirements">Must be at least 8 characters with one number.</p>
错误处理(3.3.1、3.3.3)

<form novalidate>
  <div class="field" aria-live="polite">
    <label for="email">Email</label>
    <input type="email" id="email" aria-invalid="true" aria-describedby="email-error" />
    <p id="email-error" class="error" role="alert">Please enter a valid email address (e.g., [email protected])</p>
  </div>
</form>
// Focus first error on submit
form.addEventListener('submit', (e) => {
  const firstError = form.querySelector('[aria-invalid="true"]')
  if (firstError) {
    e.preventDefault()
    firstError.focus()

    // Announce error summary
    const errorSummary = document.getElementById('error-summary')
    errorSummary.textContent = `${errors.length} errors found. Please fix them and try again.`
    errorSummary.focus()
  }
})

兼容性

有效的 HTML(4.1.1)

<div id="content">...</div>
<div id="content">...</div>


<a href="/"><button>Click</button></a>


<div id="main-content">...</div>
<div id="sidebar-content">...</div>


<a href="/" class="button-link">Click</a>
ARIA 用法(4.1.2)

优先使用原生元素:


<div role="button" tabindex="0">Click me</div>


<button>Click me</button>


<div role="checkbox" aria-checked="false">Option</div>


<label><input type="checkbox" /> Option</label>

需要使用 ARIA 时:


<div role="tablist" aria-label="Product information">
  <button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1">Description</button>
  <button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2" tabindex="-1">Reviews</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  
</div>
实时区域(4.1.3)

<div aria-live="polite" aria-atomic="true" class="status">
  
</div>


<div role="alert" aria-live="assertive">
  
</div>
// Announce dynamic content changes
function showNotification(message, type = 'polite') {
  const container = document.getElementById(`${type}-announcer`)
  container.textContent = '' // Clear first
  requestAnimationFrame(() => {
    container.textContent = message
  })
}

测试清单

自动化测试
# Lighthouse accessibility audit
npx lighthouse https://example.com --only-categories=accessibility

# axe-core
npm install @axe-core/cli -g
axe https://example.com
手动测试
  • 键盘导航: 使用 Tab 遍历整个页面,使用 Enter/Space 激活
  • 屏幕阅读器: 使用 VoiceOver(Mac)、NVDA(Windows)或 TalkBack(Android)进行测试
  • 缩放: 内容在 200% 缩放下仍可使用
  • 高对比度: 使用 Windows 高对比度模式进行测试
  • 减少动效: 使用 prefers-reduced-motion: reduce 进行测试
  • 焦点顺序: 符合逻辑,并与视觉顺序一致
屏幕阅读器命令
操作VoiceOver(Mac)NVDA(Windows)
启动/停止⌘ + F5Ctrl + Alt + N
下一项VO + →↓
上一项VO + ←↑
激活VO + SpaceEnter
标题列表VO + U,然后按方向键H / Shift + H
链接列表VO + UK / Shift + K

按影响程度分类的常见问题

严重(立即修复)
  1. 缺少表单标签
  2. 图片缺少替代文本
  3. 色彩对比度不足
  4. 键盘陷阱
  5. 没有焦点指示器
重要(发布前修复)
  1. 缺少页面语言
  2. 缺少标题结构
  3. 链接文本描述不清
  4. 媒体自动播放
  5. 缺少跳过链接
中等(尽快修复)
  1. 图标缺少 ARIA 标签
  2. 导航不一致
  3. 未标识错误
  4. 时间限制没有控制选项
  5. 缺少地标区域

References

评论

支持 Markdown