accessibility

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Accessibility (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

PrincipleDescription
PerceivableContent can be perceived through different senses
OperableInterface can be operated by all users
UnderstandableContent and interface are understandable
RobustContent works with assistive technologies
原则描述
Perceivable(可感知)内容可通过多种感官被感知
Operable(可操作)界面可供所有用户操作
Understandable(可理解)内容和界面易于理解
Robust(健壮性)内容可与辅助技术兼容

Conformance levels

合规等级

LevelRequirementTarget
AMinimum accessibilityMust pass
AAStandard complianceShould pass (legal requirement in many jurisdictions)
AAAEnhanced accessibilityNice 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 SizeAA minimumAAA enhanced
Normal text (< 18px / < 14px bold)4.5:17:1
Large text (≥ 18px / ≥ 14px bold)3:14.5:1
UI components & graphics3:13: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:17:1
大文本(≥18px / ≥14px粗体)3:14.5:1
UI组件与图形3:13: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
undefined
bash
undefined

Lighthouse 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
undefined
npm install @axe-core/cli -g axe https://example.com
undefined

Manual 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

屏幕阅读器常用指令

ActionVoiceOver (Mac)NVDA (Windows)
Start/Stop⌘ + F5Ctrl + Alt + N
Next itemVO + →
Previous itemVO + ←
ActivateVO + SpaceEnter
Headings listVO + U, then arrowsH / Shift + H
Links listVO + UK / Shift + K

操作VoiceOver(Mac)NVDA(Windows)
启动/停止⌘ + F5Ctrl + Alt + N
下一个元素VO + →
上一个元素VO + ←
激活元素VO + SpaceEnter
标题列表VO + U,然后用方向键H / Shift + H
链接列表VO + UK / Shift + K

Common issues by impact

常见问题及影响等级

Critical (fix immediately)

严重(立即修复)

  1. Missing form labels
  2. Missing image alt text
  3. Insufficient color contrast
  4. Keyboard traps
  5. No focus indicators
  1. 缺少表单标签
  2. 缺少图片alt文本
  3. 色彩对比度不足
  4. 键盘陷阱
  5. 无焦点指示器

Serious (fix before launch)

重要(上线前修复)

  1. Missing page language
  2. Missing heading structure
  3. Non-descriptive link text
  4. Auto-playing media
  5. Missing skip links
  1. 未指定页面语言
  2. 缺少标题层级结构
  3. 链接文本无描述性
  4. 自动播放媒体
  5. 缺少跳转链接

Moderate (fix soon)

中等(尽快修复)

  1. Missing ARIA labels on icons
  2. Inconsistent navigation
  3. Missing error identification
  4. Timing without controls
  5. Missing landmark regions
  1. 图标缺少ARIA标签
  2. 导航不一致
  3. 错误未明确标识
  4. 时间限制无控制选项
  5. 缺少地标区域

References

参考资料