Web 无障碍设计实践指南

去年项目被客户投诉”盲人无法使用”,才开始重视无障碍。现在成了团队的必须项。

为什么重要

原因说明
法律要求很多国家有法律要求
用户群体全球 15% 人口有某种障碍
SEO 好处语义化对搜索引擎友好
代码质量无障碍要求强迫你写规范代码

WCAG 原则

四大原则(POUR):

原则英文说明
可感知Perceivable信息必须能被感知
可操作Operable界面必须能被操作
可理解Understandable内容必须容易理解
健壮Robust兼容各种辅助技术

常见问题和解决

1. 图片缺少替代文本

<!-- 不好 -->
<img src="chart.png">

<!-- 好 -->
<img src="chart.png" alt="2024年销售额增长趋势图,从100万增长到150万">

原则:描述图片传达的信息,不是简单说”图片”。

2. 表单没有标签

<!-- 不好 -->
<input type="text" placeholder="用户名">

<!-- 好 -->
<label for="username">用户名</label>
<input type="text" id="username" name="username">

屏幕阅读器需要 label 来告诉用户这是什么字段。

3. 按钮语义不正确

<!-- 不好:用 div 当按钮 -->
<div onclick="submit()">提交</div>

<!-- 好 -->
<button type="button" onclick="submit()">提交</button>

div 没有按钮的语义,键盘无法聚焦,屏幕阅读器不会识别。

4. 键盘无法操作

<!-- 不好 -->
<div onclick="openModal()">打开弹窗</div>

<!-- 好 -->
<button onclick="openModal()">打开弹窗</button>

<!-- 或者添加键盘支持 -->
<div
  onclick="openModal()"
  onkeydown="if(event.key === 'Enter') openModal()"
  tabindex="0"
  role="button"
>
  打开弹窗
</div>

无障碍检测

ARIA 属性

ARIA 是 HTML 语义的补充。

常用属性

属性用途
role定义元素角色
aria-label提供标签
aria-labelledby关联标签
aria-describedby关联描述
aria-hidden隐藏辅助技术
aria-expanded展开状态
aria-live动态内容通知

示例:自定义下拉菜单

<div class="dropdown">
  <button
    id="dropdown-trigger"
    aria-haspopup="listbox"
    aria-expanded="false"
    aria-controls="dropdown-menu"
  >
    选择选项
  </button>

  <ul
    id="dropdown-menu"
    role="listbox"
    aria-labelledby="dropdown-trigger"
    hidden
  >
    <li role="option" aria-selected="false">选项一</li>
    <li role="option" aria-selected="true">选项二</li>
    <li role="option" aria-selected="false">选项三</li>
  </ul>
</div>
// 切换展开状态
function toggleDropdown() {
  const menu = document.getElementById('dropdown-menu');
  const trigger = document.getElementById('dropdown-trigger');
  const isExpanded = trigger.getAttribute('aria-expanded') === 'true';

  trigger.setAttribute('aria-expanded', !isExpanded);
  menu.hidden = isExpanded;
}

示例:实时通知

<div aria-live="polite" aria-atomic="true" class="notifications">
  <!-- 动态内容会被读出 -->
</div>

polite 表示不打断当前操作,assertive 会立即读出。

焦点管理

焦点顺序

Tab 键遍历顺序应该是逻辑顺序:

<!-- 不好:视觉顺序和 DOM 顺序不一致 -->
<div style="display: flex; flex-direction: row-reverse;">
  <button>步骤3</button>
  <button>步骤2</button>
  <button>步骤1</button>
</div>

<!-- 好:调整 DOM 顺序 -->
<div>
  <button>步骤1</button>
  <button>步骤2</button>
  <button>步骤3</button>
</div>

焦点可见

/* 不要移除焦点样式 */
button:focus {
  outline: none; /* 不好 */
}

/* 自定义焦点样式 */
button:focus {
  outline: 2px solid blue;
  outline-offset: 2px;
}

模态框焦点

打开模态框后,焦点应该在模态框内。

function openModal(modal) {
  modal.showModal();

  // 焦点移到模态框
  const firstFocusable = modal.querySelector('button, [href], input, select, textarea');
  if (firstFocusable) {
    firstFocusable.focus();
  }

  // 焦点陷阱
  modal.addEventListener('keydown', trapFocus);
}

function trapFocus(e) {
  if (e.key !== 'Tab') return;

  const focusables = e.currentTarget.querySelectorAll('button, [href], input, select, textarea');
  const first = focusables[0];
  const last = focusables[focusables.length - 1];

  if (e.shiftKey && document.activeElement === first) {
    e.preventDefault();
    last.focus();
  } else if (!e.shiftKey && document.activeElement === last) {
    e.preventDefault();
    first.focus();
  }
}

颜色对比度

文字和背景要有足够对比度。

级别对比度要求
AA(最低)4.5:1(普通文字)/ 3:1(大字)
AAA7:1(普通文字)/ 4.5:1(大字)

检测工具:Chrome DevTools → Accessibility

对比度检测

检测工具

工具用途
Chrome DevTools即时检测
axe DevTools浏览器插件
Lighthouse综合评分
WAVE页面可视化
NVDA/VoiceOver屏幕阅读器测试

团队实践

代码审查清单

- [ ] 图片有 alt 属性
- [ ] 表单有 label
- [ ] 按钮使用正确语义
- [ ] 键盘可以操作
- [ ] 颜色对比度足够
- [ ] 焦点可见
- [ ] 动态内容有 aria-live

CI 检查

- name: Accessibility Check
  run: npm run test:a11y
// 使用 axe-core
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

test('should have no accessibility violations', async () => {
  const { container } = render(<Button>Click me</Button>);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

总结

无障碍不是额外工作,是开发的一部分。

关键点:

  1. 使用正确语义
  2. 支持键盘操作
  3. 提供替代文本
  4. 管理好焦点
  5. 保持足够对比度

做了无障碍后,代码质量也提高了。这是个良性循环。