mcpField
A
accessibility

accessibility

github.com/davila7设计与 UI33.0k358 次安装1 天前更新
代码仓库

关于 accessibility

accessibility 是供设计和开发人员参考的无障碍技能,依据 WCAG 2.1 和 Lighthouse 无障碍审计指南。内容涵盖无障碍原则、实现示例,以及自动化和手动测试方法。

主要功能

  1. 01应用 WCAG POUR 原则
  2. 02检查对比度与文本替代内容
  3. 03设计键盘可操作的交互
  4. 04处理焦点、表单和错误
  5. 05执行自动化与手动检查

适用场景

  1. 01依据 WCAG 指南审查网页
  2. 02改善键盘导航和焦点显示
  3. 03检查表单标签与错误反馈
  4. 04使用无障碍工具测试页面

安装

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

$npx skills add https://github.com/davila7/claude-code-templates --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>
一致的导航(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/空格键激活
  • 屏幕阅读器: 使用 VoiceOver(Mac)、NVDA(Windows)或 TalkBack(Android)进行测试
  • 缩放: 内容在 200% 缩放下仍可使用
  • 高对比度: 使用 Windows 高对比度模式进行测试
  • 减弱动效: 使用 prefers-reduced-motion: reduce 进行测试
  • 焦点顺序: 逻辑合理且遵循视觉顺序
屏幕阅读器命令
操作VoiceOver(Mac)NVDA(Windows)
启动/停止⌘ + F5Ctrl + Alt + N
下一项VO + →↓
上一项VO + ←↑
激活VO + 空格Enter
标题列表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. 缺少地标区域

参考资料

评论

支持 Markdown