无障碍设计Skill accessibility

这个技能用于审计和改善网站的无障碍访问,遵循WCAG 2.1指南,确保内容对所有用户包括残障人士可用。涉及文本替代、颜色对比、键盘导航、屏幕阅读器支持等关键方面。关键词:无障碍访问、WCAG、屏幕阅读器、键盘导航、a11y、颜色对比、ARIA、HTML有效性。

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

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

无障碍访问 (a11y)

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

WCAG原则: POUR

原则 描述
Perceivable 可感知 内容可以通过不同感官感知
Operable 可操作 界面可以被所有用户操作
Understandable 可理解 内容和界面可理解
Robust 健壮 内容与辅助技术兼容

合规级别

级别 要求 目标
A 最低可访问性 必须通过
AA 标准合规 应通过(在许多司法管辖区是法律要求)
AAA 增强可访问性 锦上添花

可感知

文本替代 (1.1)

图像需要alt文本:

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

<!-- ✅ 描述性alt -->
<img src="chart.png" alt="条形图显示第三季度销售额增长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>

一致性导航 (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="Email">

<!-- ✅ 显式标签 -->
<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)

首选原生元素:

<!-- ❌ ARIA 角色在 div 上 -->
<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. 缺少地标区域

参考