无障碍设计 A11y 完全指南

HTMLPAGE 团队
22 分钟阅读

学习 WCAG 标准、屏幕阅读器兼容性、键盘导航等无障碍设计要素

无障碍设计 (A11y) 完全指南

无障碍设计确保所有用户(包括残障人士)都能使用你的产品。这不仅是道德责任,也是法律要求。

核心无障碍原则

WCAG 2.1 标准

<!-- 1. 可感知性 (Perceivable) -->
<!-- 提供替代文本 -->
<img src="chart.png" alt="2024年销售增长图表,显示 45% 增长">

<!-- 提供字幕 -->
<video controls>
  <source src="video.mp4">
  <track kind="captions" src="captions.vtt" srclang="en">
</video>

<!-- 2. 可操作性 (Operable) -->
<!-- 键盘导航 -->
<button onclick="handleClick()">确认</button>

<!-- 3. 可理解性 (Understandable) -->
<!-- 清晰的标签 -->
<label for="email">邮箱地址</label>
<input id="email" type="email" placeholder="user@example.com">

<!-- 4. 鲁棒性 (Robust) -->
<!-- 使用语义 HTML -->
<article>
  <header>
    <h1>标题</h1>
  </header>
  <main>内容</main>
  <footer>页脚</footer>
</article>

无障碍代码示例

语义化 HTML

<!-- ✅ 推荐:使用语义标签 -->
<nav>导航</nav>
<main>主要内容</main>
<article>文章</article>
<aside>侧边栏</aside>
<section>章节</section>

<!-- ❌ 避免:过度使用 div -->
<div class="nav">导航</div>
<div class="main">主要内容</div>

<!-- 正确的表单结构 -->
<form>
  <fieldset>
    <legend>个人信息</legend>
    <div>
      <label for="name">姓名:</label>
      <input id="name" type="text" required>
    </div>
    <div>
      <label for="email">邮箱:</label>
      <input id="email" type="email" required>
    </div>
  </fieldset>
</form>

ARIA 属性

<!-- ARIA 角色和属性 -->
<div role="button" tabindex="0" onclick="handleClick()">
  可点击的 div
</div>

<!-- ARIA 标签 -->
<button aria-label="关闭对话框">×</button>

<!-- ARIA 状态 -->
<button aria-pressed="false" onclick="toggle(this)">
  切换
</button>

<!-- ARIA 描述 -->
<input type="password" aria-describedby="pwd-hint">
<div id="pwd-hint">
  密码必须至少 8 个字符,包含大小写字母和数字
</div>

<!-- ARIA 实时区域 -->
<div aria-live="polite" aria-atomic="true">
  <!-- 动态更新的内容将自动宣布 -->
</div>

色彩对比度

/* WCAG AA 级别:4.5:1 对比度 */
body {
  color: #000;
  background: #fff;
  /* 对比度: 21:1 ✅ */
}

/* 不足的对比度 */
.weak-contrast {
  color: #999;  /* 不好 */
  background: #fff;
  /* 对比度: 2.5:1 ❌ */
}

/* 改进的对比度 */
.good-contrast {
  color: #555;
  background: #fff;
  /* 对比度: 8:1 ✅ */
}

/* 使用 CSS 变量管理颜色 */
:root {
  --text-primary: #000;
  --text-secondary: #333;
  --bg-primary: #fff;
  --bg-secondary: #f5f5f5;
}

body {
  color: var(--text-primary);
  background: var(--bg-primary);
}

键盘导航

// 处理键盘事件
document.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ') {
    handleActivation(e.target);
  }
  
  if (e.key === 'Escape') {
    closeModal();
  }
  
  if (e.key === 'Tab') {
    // 管理焦点顺序
    manageFocus(e);
  }
});

// 确保自定义按钮支持键盘
const customButton = document.querySelector('[role="button"]');
customButton.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault();
    customButton.click();
  }
});

// 焦点管理
function manageFocusTrap(element) {
  const focusableElements = element.querySelectorAll(
    'button, a, input, select, textarea, [tabindex]'
  );
  
  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];
  
  element.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();
      }
    }
  });
}

屏幕阅读器优化

<!-- 跳转链接 -->
<a href="#main-content" class="skip-link">
  跳转到主内容
</a>

<style>
  .skip-link {
    position: absolute;
    left: -9999px;
  }
  
  .skip-link:focus {
    left: 0;
    z-index: 1000;
  }
</style>

<!-- 隐藏装饰性元素 -->
<img src="decorative-line.svg" alt="">
<span aria-hidden="true">●</span>

<!-- 提供上下文 -->
<a href="/page">了解更多<span class="sr-only">关于 React 最佳实践</span></a>

<!-- 屏幕阅读器专用文本 -->
<style>
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0,0,0,0);
    white-space: nowrap;
    border: 0;
  }
</style>

完整的无障碍组件示例

// React 无障碍对话框
import { useEffect, useRef } from 'react';

function AccessibleModal({ isOpen, onClose, title, children }) {
  const modalRef = useRef(null);
  
  useEffect(() => {
    if (isOpen) {
      // 焦点陷阱
      const focusableElements = modalRef.current.querySelectorAll(
        'button, a, input, [tabindex]'
      );
      const firstElement = focusableElements[0];
      firstElement?.focus();
    }
  }, [isOpen]);
  
  return (
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
      aria-hidden={!isOpen}
    >
      <h2 id="modal-title">{title}</h2>
      {children}
      <button
        aria-label="关闭对话框"
        onClick={onClose}
      >
        ×
      </button>
    </div>
  );
}

// 无障碍下拉菜单
function AccessibleDropdown({ label, options }) {
  return (
    <div>
      <label htmlFor="dropdown">{label}</label>
      <select id="dropdown" aria-label={label}>
        {options.map(opt => (
          <option key={opt.value} value={opt.value}>
            {opt.label}
          </option>
        ))}
      </select>
    </div>
  );
}

测试无障碍性

自动化测试工具

# 使用 axe 进行自动化测试
npm install --save-dev @axe-core/react

# 在浏览器中安装 axe DevTools 扩展
# Chrome: https://chrome.google.com/webstore/...

# 使用 Lighthouse 审计
# Chrome DevTools > Lighthouse > 可访问性

手动测试

// 检查清单
const a11yChecklist = [
  'All images have alt text',
  'Color is not the only means of conveying information',
  'All form inputs have labels',
  'Page is keyboard navigable',
  'Focus indicators are visible',
  'Text has sufficient contrast (4.5:1)',
  'Content is organized with headings',
  'Links have descriptive text',
  'Video has captions',
  'Interactive elements are large enough (44x44px)',
];

最佳实践

✅ 应该做的事

  1. 使用语义 HTML
    • <button> 代替 <div role="button">
    • <nav>, <main>, <article>
    • 正确的标题层级
  2. 提供替代文本
    • 有意义的 alt 文本
    • 实时字幕和摘要
    • 音频转录
  3. 确保键盘可访问
    • 所有功能都可用键盘操作
    • 清晰的焦点指示
    • 合理的 Tab 顺序
  4. 测试真实用户
    • 与残障用户测试
    • 使用屏幕阅读器
    • 键盘导航测试

❌ 不应该做的事

  1. 颜色依赖
    • 不要仅用颜色表达信息
    • 总是补充文本或图标
  2. 自动播放
    • 不要自动播放音视频
    • 让用户控制播放
  3. 闪烁内容
    • 避免每秒闪烁超过 3 次
    • 可能引发癫痫发作
  4. 忽视焦点管理
    • 一定要提供可见的焦点指示
    • 管理模态框中的焦点

总结

无障碍设计是包容性产品设计的基础,它使所有用户都能受益。