名称: 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 |
常见问题按影响
关键 (立即修复)
- 缺少表单标签
- 缺少图像 alt 文本
- 颜色对比不足
- 键盘陷阱
- 无焦点指示器
严重 (发布前修复)
- 缺少页面语言
- 缺少标题结构
- 非描述性链接文本
- 自动播放媒体
- 缺少跳过链接
中等 (尽快修复)
- 图标上缺少 ARIA 标签
- 不一致导航
- 缺少错误标识
- 无控件的计时
- 缺少地标区域