accessibility
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAccessibility (a11y)
网页可访问性(a11y)
Comprehensive accessibility guidelines based on WCAG 2.1 and Lighthouse accessibility audits. Goal: make content usable by everyone, including people with disabilities.
基于WCAG 2.1和Lighthouse可访问性审核的全面指南。目标:让所有人(包括残障人士)都能使用内容。
WCAG Principles: POUR
WCAG核心原则:POUR
| Principle | Description |
|---|---|
| Perceivable | Content can be perceived through different senses |
| Operable | Interface can be operated by all users |
| Understandable | Content and interface are understandable |
| Robust | Content works with assistive technologies |
| 原则 | 描述 |
|---|---|
| Perceivable(可感知) | 内容可通过多种感官被感知 |
| Operable(可操作) | 界面可供所有用户操作 |
| Understandable(可理解) | 内容和界面易于理解 |
| Robust(健壮性) | 内容可与辅助技术兼容 |
Conformance levels
合规等级
| Level | Requirement | Target |
|---|---|---|
| A | Minimum accessibility | Must pass |
| AA | Standard compliance | Should pass (legal requirement in many jurisdictions) |
| AAA | Enhanced accessibility | Nice to have |
| 等级 | 要求 | 目标 |
|---|---|---|
| A | 基础可访问性 | 必须达标 |
| AA | 标准合规 | 建议达标(在许多地区属于法律要求) |
| AAA | 增强可访问性 | 可选优化 |
Perceivable
可感知性
Text alternatives (1.1)
文本替代方案(1.1)
Images require alt text:
html
<!-- ❌ Missing alt -->
<img src="chart.png">
<!-- ✅ Descriptive alt -->
<img src="chart.png" alt="Bar chart showing 40% increase in Q3 sales">
<!-- ✅ Decorative image (empty alt) -->
<img src="decorative-border.png" alt="" role="presentation">
<!-- ✅ Complex image with longer description -->
<figure>
<img src="infographic.png" alt="2024 market trends infographic"
aria-describedby="infographic-desc">
<figcaption id="infographic-desc">
<!-- Detailed description -->
</figcaption>
</figure>Icon buttons need accessible names:
html
<!-- ❌ No accessible name -->
<button><svg><!-- menu icon --></svg></button>
<!-- ✅ Using aria-label -->
<button aria-label="Open menu">
<svg aria-hidden="true"><!-- menu icon --></svg>
</button>
<!-- ✅ Using visually hidden text -->
<button>
<svg aria-hidden="true"><!-- menu icon --></svg>
<span class="visually-hidden">Open menu</span>
</button>Visually hidden class:
css
.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;
}图片需添加alt文本:
html
<!-- ❌ 缺少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>图标按钮需设置可访问名称:
html
<!-- ❌ 无可访问名称 -->
<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>视觉隐藏类:
css
.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;
}Color contrast (1.4.3, 1.4.6)
色彩对比度(1.4.3, 1.4.6)
| Text Size | AA minimum | AAA enhanced |
|---|---|---|
| Normal text (< 18px / < 14px bold) | 4.5:1 | 7:1 |
| Large text (≥ 18px / ≥ 14px bold) | 3:1 | 4.5:1 |
| UI components & graphics | 3:1 | 3:1 |
css
/* ❌ 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;
}Don't rely on color alone:
html
<!-- ❌ Only color indicates error -->
<input class="error-border">
<style>.error-border { border-color: red; }</style>
<!-- ✅ Color + icon + text -->
<div class="field-error">
<input aria-invalid="true" aria-describedby="email-error">
<span id="email-error" class="error-message">
<svg aria-hidden="true"><!-- error icon --></svg>
Please enter a valid email address
</span>
</div>| 文本尺寸 | AA级最低要求 | AAA级增强要求 |
|---|---|---|
| 常规文本(<18px / <14px粗体) | 4.5:1 | 7:1 |
| 大文本(≥18px / ≥14px粗体) | 3:1 | 4.5:1 |
| UI组件与图形 | 3:1 | 3:1 |
css
/* ❌ 低对比度(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;
}不要仅依赖颜色传递信息:
html
<!-- ❌ 仅用颜色标识错误 -->
<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>Media alternatives (1.2)
媒体替代方案(1.2)
html
<!-- Video with captions -->
<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 with transcript -->
<audio controls>
<source src="podcast.mp3" type="audio/mp3">
</audio>
<details>
<summary>Transcript</summary>
<p>Full transcript text...</p>
</details>html
<!-- 带字幕的视频 -->
<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>Operable
可操作性
Keyboard accessible (2.1)
键盘可访问(2.1)
All functionality must be keyboard accessible:
javascript
// ❌ 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();
}
});No keyboard traps:
javascript
// 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();
}所有功能必须支持键盘操作:
javascript
// ❌ 仅支持点击事件
element.addEventListener('click', handleAction);
// ✅ 同时支持点击和键盘操作
element.addEventListener('click', handleAction);
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleAction();
}
});避免键盘陷阱:
javascript
// 模态框焦点管理
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();
}Focus visible (2.4.7)
焦点可见(2.4.7)
css
/* ❌ 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);
}css
/* ❌ 永远不要移除焦点轮廓 */
*: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);
}Skip links (2.4.1)
跳转链接(2.4.1)
html
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header><!-- navigation --></header>
<main id="main-content" tabindex="-1">
<!-- main content -->
</main>
</body>css
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px 16px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}html
<body>
<a href="#main-content" class="skip-link">跳转到主要内容</a>
<header><!-- 导航栏 --></header>
<main id="main-content" tabindex="-1">
<!-- 主要内容 -->
</main>
</body>css
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px 16px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}Timing (2.2)
时间限制(2.2)
javascript
// 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
});
}javascript
// 允许用户延长时间限制
function showSessionWarning() {
const modal = createModal({
title: '会话即将过期',
content: '您的会话将在2分钟后过期。',
actions: [
{ label: '延长会话', action: extendSession },
{ label: '退出登录', action: logout }
],
timeout: 120000 // 2分钟响应时间
});
}Motion (2.3)
动画控制(2.3)
css
/* 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;
}
}css
/* 尊重用户减少动画的偏好 */
@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;
}
}Understandable
可理解性
Page language (3.1.1)
页面语言(3.1.1)
html
<!-- ❌ No language specified -->
<html>
<!-- ✅ Language specified -->
<html lang="en">
<!-- ✅ Language changes within page -->
<p>The French word for hello is <span lang="fr">bonjour</span>.</p>html
<!-- ❌ 未指定页面语言 -->
<html>
<!-- ✅ 指定页面语言 -->
<html lang="en">
<!-- ✅ 页面内语言切换 -->
<p>法语里的你好是 <span lang="fr">bonjour</span>。</p>Consistent navigation (3.2.3)
导航一致性(3.2.3)
html
<!-- Navigation should be consistent across pages -->
<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>html
<!-- 导航在所有页面应保持一致 -->
<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>Form labels (3.3.2)
表单标签(3.3.2)
html
<!-- ❌ No label association -->
<input type="email" placeholder="Email">
<!-- ✅ Explicit label -->
<label for="email">Email address</label>
<input type="email" id="email" name="email"
autocomplete="email" required>
<!-- ✅ Implicit label -->
<label>
Email address
<input type="email" name="email" autocomplete="email" required>
</label>
<!-- ✅ With instructions -->
<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>html
<!-- ❌ 未关联标签 -->
<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>Error handling (3.3.1, 3.3.3)
错误处理(3.3.1, 3.3.3)
html
<!-- Announce errors to screen readers -->
<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., name@example.com)
</p>
</div>
</form>javascript
// 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
<!-- 向屏幕阅读器播报错误信息 -->
<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>javascript
// 提交时聚焦第一个错误字段
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();
}
});Robust
健壮性
Valid HTML (4.1.1)
有效HTML(4.1.1)
html
<!-- ❌ Duplicate IDs -->
<div id="content">...</div>
<div id="content">...</div>
<!-- ❌ Invalid nesting -->
<a href="/"><button>Click</button></a>
<!-- ✅ Unique IDs -->
<div id="main-content">...</div>
<div id="sidebar-content">...</div>
<!-- ✅ Proper nesting -->
<a href="/" class="button-link">Click</a>html
<!-- ❌ 重复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 usage (4.1.2)
ARIA使用规范(4.1.2)
Prefer native elements:
html
<!-- ❌ ARIA role on div -->
<div role="button" tabindex="0">Click me</div>
<!-- ✅ Native button -->
<button>Click me</button>
<!-- ❌ ARIA checkbox -->
<div role="checkbox" aria-checked="false">Option</div>
<!-- ✅ Native checkbox -->
<label><input type="checkbox"> Option</label>When ARIA is needed:
html
<!-- Custom tabs component -->
<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">
<!-- Panel content -->
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
<!-- Panel content -->
</div>优先使用原生元素:
html
<!-- ❌ 在div上使用ARIA按钮角色 -->
<div role="button" tabindex="0">点击我</div>
<!-- ✅ 使用原生按钮 -->
<button>点击我</button>
<!-- ❌ 使用ARIA复选框 -->
<div role="checkbox" aria-checked="false">选项</div>
<!-- ✅ 使用原生复选框 -->
<label><input type="checkbox"> 选项</label>必要时使用ARIA:
html
<!-- 自定义标签页组件 -->
<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>Live regions (4.1.3)
实时区域(4.1.3)
html
<!-- Status updates -->
<div aria-live="polite" aria-atomic="true" class="status">
<!-- Content updates announced to screen readers -->
</div>
<!-- Urgent alerts -->
<div role="alert" aria-live="assertive">
<!-- Interrupts current announcement -->
</div>javascript
// Announce dynamic content changes
function showNotification(message, type = 'polite') {
const container = document.getElementById(`${type}-announcer`);
container.textContent = ''; // Clear first
requestAnimationFrame(() => {
container.textContent = message;
});
}html
<!-- 状态更新 -->
<div aria-live="polite" aria-atomic="true" class="status">
<!-- 内容更新会被屏幕阅读器播报 -->
</div>
<!-- 紧急通知 -->
<div role="alert" aria-live="assertive">
<!-- 会打断当前播报内容 -->
</div>javascript
// 播报动态内容变化
function showNotification(message, type = 'polite') {
const container = document.getElementById(`${type}-announcer`);
container.textContent = ''; // 先清空
requestAnimationFrame(() => {
container.textContent = message;
});
}Testing checklist
测试清单
Automated testing
自动化测试
bash
undefinedbash
undefinedLighthouse accessibility audit
Lighthouse可访问性审核
npx lighthouse https://example.com --only-categories=accessibility
npx lighthouse https://example.com --only-categories=accessibility
axe-core
axe-core
npm install @axe-core/cli -g
axe https://example.com
undefinednpm install @axe-core/cli -g
axe https://example.com
undefinedManual testing
手动测试
- Keyboard navigation: Tab through entire page, use Enter/Space to activate
- Screen reader: Test with VoiceOver (Mac), NVDA (Windows), or TalkBack (Android)
- Zoom: Content usable at 200% zoom
- High contrast: Test with Windows High Contrast Mode
- Reduced motion: Test with
prefers-reduced-motion: reduce - Focus order: Logical and follows visual order
- 键盘导航: 按Tab键遍历整个页面,使用Enter/空格键激活元素
- 屏幕阅读器: 使用VoiceOver(Mac)、NVDA(Windows)或TalkBack(Android)测试
- 缩放测试: 内容在200%缩放后仍可正常使用
- 高对比度模式: 在Windows高对比度模式下测试
- 减少动画: 开启测试
prefers-reduced-motion: reduce - 焦点顺序: 焦点顺序符合逻辑且与视觉顺序一致
Screen reader commands
屏幕阅读器常用指令
| Action | VoiceOver (Mac) | NVDA (Windows) |
|---|---|---|
| Start/Stop | ⌘ + F5 | Ctrl + Alt + N |
| Next item | VO + → | ↓ |
| Previous item | VO + ← | ↑ |
| Activate | VO + Space | Enter |
| Headings list | VO + U, then arrows | H / Shift + H |
| Links list | VO + U | K / Shift + K |
| 操作 | VoiceOver(Mac) | NVDA(Windows) |
|---|---|---|
| 启动/停止 | ⌘ + F5 | Ctrl + Alt + N |
| 下一个元素 | VO + → | ↓ |
| 上一个元素 | VO + ← | ↑ |
| 激活元素 | VO + Space | Enter |
| 标题列表 | VO + U,然后用方向键 | H / Shift + H |
| 链接列表 | VO + U | K / Shift + K |
Common issues by impact
常见问题及影响等级
Critical (fix immediately)
严重(立即修复)
- Missing form labels
- Missing image alt text
- Insufficient color contrast
- Keyboard traps
- No focus indicators
- 缺少表单标签
- 缺少图片alt文本
- 色彩对比度不足
- 键盘陷阱
- 无焦点指示器
Serious (fix before launch)
重要(上线前修复)
- Missing page language
- Missing heading structure
- Non-descriptive link text
- Auto-playing media
- Missing skip links
- 未指定页面语言
- 缺少标题层级结构
- 链接文本无描述性
- 自动播放媒体
- 缺少跳转链接
Moderate (fix soon)
中等(尽快修复)
- Missing ARIA labels on icons
- Inconsistent navigation
- Missing error identification
- Timing without controls
- Missing landmark regions
- 图标缺少ARIA标签
- 导航不一致
- 错误未明确标识
- 时间限制无控制选项
- 缺少地标区域
References
参考资料
- WCAG 2.1 Quick Reference
- WAI-ARIA Authoring Practices
- Deque axe Rules
- Web Quality Audit