前端可访问性开发Skill accessibility

这个技能用于审计和提升网站的可访问性,遵循WCAG 2.1指南,确保内容对所有用户(包括残障人士)都可访问。它涵盖文本替代、颜色对比、键盘导航、屏幕阅读器支持等关键方面,帮助开发者创建无障碍的Web应用。关键词:Web可访问性、WCAG 2.1、无障碍开发、前端可访问性、A11Y、屏幕阅读器支持、键盘导航。

前端开发 0 次安装 0 次浏览 更新于 3/15/2026

名称: 可访问性 描述: 审计和改进Web可访问性,遵循WCAG 2.1指南。当被要求“改进可访问性”、“a11y审计”、“WCAG合规”、“屏幕阅读器支持”、“键盘导航”或“使其可访问”时使用。 许可证: MIT 元数据: 作者: web-quality-skills 版本: ‘1.0’

可访问性 (a11y)

基于WCAG 2.1和Lighthouse可访问性审计的全面可访问性指南。目标:使内容对所有人(包括残障人士)可用。

WCAG原则: POUR

原则 描述
P可感知 内容可以通过不同感官感知
O可操作 界面可以被所有用户操作
U可理解 内容和界面可理解
R健壮 内容与辅助技术配合工作

合规级别

级别 要求 目标
A 最低可访问性 必须通过
AA 标准合规 应通过(许多司法管辖区的法律要求)
AAA 增强的可访问性 最好有

可感知

文本替代 (1.1)

图像需要alt文本:

<!-- ❌ 缺少alt -->
<img src="chart.png" />

<!-- ✅ 描述性alt -->
<img src="chart.png" alt="条形图显示Q3销售额增长40%" />

<!-- ✅ 装饰性图像(空alt) -->
<img src="decorative-border.png" alt="" role="presentation" />

<!-- ✅ 复杂图像带长描述 -->
<figure>
  <img src="infographic.png" alt="2024市场趋势信息图" aria-describedby="infographic-desc" />
  <figcaption id="infographic-desc">
    <!-- 详细描述 -->
  </figcaption>
</figure>

图标按钮需要可访问名称:

<!-- ❌ 无可访问名称 -->
<button>
  <svg><!-- 菜单图标 --></svg>
</button>

<!-- ✅ 使用aria-label -->
<button aria-label="打开菜单">
  <svg aria-hidden="true"><!-- 菜单图标 --></svg>
</button>

<!-- ✅ 使用视觉隐藏文本 -->
<button>
  <svg aria-hidden="true"><!-- 菜单图标 --></svg>
  <span class="visually-hidden">打开菜单</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:1 7:1
大文本 (≥ 18px / ≥ 14px粗体) 3:1 4.5:1
UI组件和图形 3:1 3:1
/* ❌ 低对比度 (2.5:1) */
.low-contrast {
  color: #999;
  background: #fff;
}

/* ✅ 足够对比度 (7:1) */
.high-contrast {
  color: #333;
  background: #fff;
}

/* ✅ 焦点状态也需要对比度 */
: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>
    请输入有效的电子邮件地址
  </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>文稿</summary>
  <p>完整文稿文本...</p>
</details>

可操作

键盘可访问 (2.1)

所有功能必须键盘可访问:

// ❌ 仅处理点击
element.addEventListener('click', handleAction)

// ✅ 处理点击和键盘
element.addEventListener('click', handleAction)
element.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault()
    handleAction()
  }
})

无键盘陷阱:

// 模态框焦点管理
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]

  // 在模态框内陷阱焦点
  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)

/* ❌ 不要移除焦点轮廓 */
*:focus {
  outline: none;
}

/* ✅ 使用 :focus-visible 用于仅键盘焦点 */
:focus {
  outline: none;
}

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

/* ✅ 或自定义焦点样式 */
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">跳转到主要内容</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)

// 允许用户延长时限
function showSessionWarning() {
  const modal = createModal({
    title: '会话即将过期',
    content: '您的会话将在2分钟内过期。',
    actions: [
      { label: '延长会话', action: extendSession },
      { label: '注销', action: logout },
    ],
    timeout: 120000, // 2分钟响应时间
  })
}

运动 (2.3)

/* 尊重减少运动偏好 */
@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>法语中hello是 <span lang="fr">bonjour</span>.</p>
  </html>
</html>

一致导航 (3.2.3)

<!-- 导航应跨页面一致 -->
<nav aria-label="主要">
  <ul>
    <li><a href="/" aria-current="page">首页</a></li>
    <li><a href="/products">产品</a></li>
    <li><a href="/about">关于</a></li>
  </ul>
</nav>

表单标签 (3.3.2)

<!-- ❌ 无标签关联 -->
<input type="email" placeholder="电子邮件" />

<!-- ✅ 显式标签 -->
<label for="email">电子邮件地址</label>
<input type="email" id="email" name="email" autocomplete="email" required />

<!-- ✅ 隐式标签 -->
<label>
  电子邮件地址
  <input type="email" name="email" autocomplete="email" required />
</label>

<!-- ✅ 带说明 -->
<label for="password">密码</label>
<input type="password" id="password" aria-describedby="password-requirements" />
<p id="password-requirements">必须至少8个字符,包含一个数字。</p>

错误处理 (3.3.1, 3.3.3)

<!-- 向屏幕阅读器宣布错误 -->
<form novalidate>
  <div class="field" aria-live="polite">
    <label for="email">电子邮件</label>
    <input type="email" id="email" aria-invalid="true" aria-describedby="email-error" />
    <p id="email-error" class="error" role="alert">请输入有效的电子邮件地址(例如,name@example.com)</p>
  </div>
</form>
// 提交时聚焦第一个错误
form.addEventListener('submit', (e) => {
  const firstError = form.querySelector('[aria-invalid="true"]')
  if (firstError) {
    e.preventDefault()
    firstError.focus()

    // 宣布错误摘要
    const errorSummary = document.getElementById('error-summary')
    errorSummary.textContent = `${errors.length} 个错误找到。请修复并重试。`
    errorSummary.focus()
  }
})

健壮

有效HTML (4.1.1)

<!-- ❌ 重复ID -->
<div id="content">...</div>
<div id="content">...</div>

<!-- ❌ 无效嵌套 -->
<a href="/"><button>点击</button></a>

<!-- ✅ 唯一ID -->
<div id="main-content">...</div>
<div id="sidebar-content">...</div>

<!-- ✅ 正确嵌套 -->
<a href="/" class="button-link">点击</a>

ARIA使用 (4.1.2)

首选原生元素:

<!-- ❌ div上ARIA角色 -->
<div role="button" tabindex="0">点击我</div>

<!-- ✅ 原生按钮 -->
<button>点击我</button>

<!-- ❌ ARIA复选框 -->
<div role="checkbox" aria-checked="false">选项</div>

<!-- ✅ 原生复选框 -->
<label><input type="checkbox" /> 选项</label>

当需要ARIA时:

<!-- 自定义标签组件 -->
<div role="tablist" aria-label="产品信息">
  <button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1">描述</button>
  <button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2" tabindex="-1">评论</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>
// 宣布动态内容变化
function showNotification(message, type = 'polite') {
  const container = document.getElementById(`${type}-announcer`)
  container.textContent = '' // 先清空
  requestAnimationFrame(() => {
    container.textContent = message
  })
}

测试清单

自动化测试

# Lighthouse可访问性审计
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)
开始/停止 ⌘ + F5 Ctrl + Alt + N
下一个项 VO + →
前一个项 VO + ←
激活 VO + Space Enter
标题列表 VO + U, 然后箭头 H / Shift + H
链接列表 VO + U K / Shift + K

按影响分类的常见问题

关键(立即修复)

  1. 缺少表单标签
  2. 缺少图像alt文本
  3. 颜色对比不足
  4. 键盘陷阱
  5. 无焦点指示器

严重(发布前修复)

  1. 缺少页面语言
  2. 缺少标题结构
  3. 非描述性链接文本
  4. 自动播放媒体
  5. 缺少跳过链接

中等(尽快修复)

  1. 缺少图标上的ARIA标签
  2. 导航不一致
  3. 缺少错误标识
  4. 无控制的计时
  5. 缺少地标区域

参考