liveBlog

无障碍开发实践:让你的网站对所有人可用

无障碍开发实践:让你的网站对所有人可用

前端开发经常忽略无障碍(Accessibility,简称 a11y)。但实际上,做无障碍不只是"道德正确",它直接影响你网站能覆盖多少用户,也和 SEO 有关系。

无障碍的核心原则

WCAG 2.1 定义了四个原则:

  1. 可感知:信息能被感知到(文本替代、字幕、对比度)
  2. 可操作:界面能被操作(键盘导航、足够的时间)
  3. 可理解:内容和 UI 可被理解(清晰的语言、可预测的行为)
  4. 健壮性:内容能被各种工具访问(语义化 HTML、ARIA)

语义化 HTML

无障碍的第一步也是最重要的一步:用正确的 HTML 标签。

<!-- 差:全用 div -->
<div class="header">
  <div class="nav">
    <div class="nav-item">首页</div>
    <div class="nav-item">关于</div>
  </div>
</div>

<!-- 好:语义化标签 -->
<header>
  <nav>
    <a href="/">首页</a>
    <a href="/about">关于</a>
  </nav>
</header>

语义化标签的好处:

  • 屏幕阅读器能正确解析页面结构
  • 键盘导航更合理
  • SEO 更好(搜索引擎能理解页面结构)

常见的语义化替换:

  • <div class="button"><button>
  • <div onclick="..."><button onClick="...">
  • <div class="img"><img>,加上 alt 文本
  • <div class="list"><ul>/<ol> + <li>
  • <div class="main"><main>

键盘可访问性

不是所有人都用鼠标。视力障碍用户、运动障碍用户、以及很多开发者习惯用键盘操作。

Tab 导航

<!-- 原生元素默认支持 Tab 导航 -->
<button>可聚焦</button>
<a href="#">可聚焦</a>
<input type="text" />

<!-- div 不可聚焦,除非加 tabindex -->
<div tabindex="0">手动添加可聚焦</div>

<!-- tabindex="-1" 可以用 JS 聚焦,但不参与 Tab 导航 -->
<div tabindex="-1" ref={ref}>...</div>

Focus 管理

模态框、抽屉这类组件打开时,焦点应该被限制在组件内部:

// 模态框打开时,焦点移到模态框内第一个可聚焦元素
function openModal() {
  modalRef.current?.showModal();
  const firstFocusable = modalRef.current?.querySelector(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  firstFocusable?.focus();
}

// 模态框关闭时,焦点回到触发按钮
function closeModal() {
  modalRef.current?.close();
  triggerRef.current?.focus();
}

键盘快捷键

useEffect(() => {
  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === 'Escape') {
      closeModal();
    }
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      focusNextItem();
    }
    if (e.key === 'ArrowUp') {
      e.preventDefault();
      focusPrevItem();
    }
  };

  document.addEventListener('keydown', handleKeyDown);
  return () => document.removeEventListener('keydown', handleKeyDown);
}, []);

颜色和对比度

WCAG AA 标准要求正常文本的对比度至少 4.5:1,大文本至少 3:1。

/* 差:灰色文字在白色背景上,对比度不够 */
.text { color: #999; background: white; }

/* 好:足够高的对比度 */
.text { color: #555; background: white; }

可以用浏览器 DevTools 检查对比度,或者用在线工具如 WebAIM Contrast Checker。

暗色模式下特别要注意对比度。很多人做的暗色主题看起来很酷,但文字看不清。

ARIA 属性

当 HTML 语义不够用时,用 ARIA 补充:

<!-- 加载状态 -->
<button aria-busy="true" aria-label="正在加载">
  <span class="spinner"></span>
</button>

<!-- 展开收起 -->
<button aria-expanded="false" aria-controls="content-1">
  更多信息
</button>
<div id="content-1" role="region" hidden>
  详细内容...
</div>

<!-- 标签页 -->
<div role="tablist">
  <button role="tab" aria-selected="true" aria-controls="panel-1">标签1</button>
  <button role="tab" aria-selected="false" aria-controls="panel-2">标签2</button>
</div>
<div role="tabpanel" id="panel-1">内容1</div>
<div role="tabpanel" id="panel-2" hidden>内容2</div>

ARIA 的原则:不要过度使用。能用原生 HTML 表达的,就不要用 ARIA。ARIA 是补充,不是替代。

图片替代文本

<!-- 有意义的图片:提供描述性的 alt -->
<img src="chart.png" alt="2025年Q1收入同比增长23%" />

<!-- 装饰性图片:空 alt -->
<img src="divider.png" alt="" role="presentation" />

<!-- 复杂图片:用 aria-describedby 关联描述 -->
<figure>
  <img src="infographic.png" alt="系统架构图" aria-describedby="fig-desc" />
  <figcaption id="fig-desc">系统由前端、API 网关、微服务三层组成...</figcaption>
</figure>

表单无障碍

<!-- 差:label 和 input 没有关联 -->
<input type="text" placeholder="用户名" />

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

<!-- 错误提示关联到输入框 -->
<input id="email" type="email" aria-invalid="true" aria-describedby="email-error" />
<span id="email-error" role="alert">请输入有效的邮箱地址</span>

role="alert" 让屏幕阅读器立即朗读错误信息,不用等待用户导航到错误位置。

React 中的无障碍

如果你用 React,注意以下几点:

  • 使用语义化 HTML,不要什么都用 div
  • 使用 aria-* 属性(React 支持所有 ARIA 属性)
  • React.Fragment 而不是 div 做包装
  • 列表渲染时提供 aria-label
  • 动态内容用 aria-live 区域
// 搜索结果用 aria-live 区域,屏幕阅读器会自动朗读
function SearchResults({ results }) {
  return (
    <div aria-live="polite" aria-atomic="true">
      {results.length === 0
        ? '没有找到结果'
        : `找到 ${results.length} 条结果`}
    </div>
  );
}

测试工具

  • axe DevTools:Chrome 插件,自动检测无障碍问题
  • Lighthouse:内置无障碍审计
  • WAVE:在线无障碍检查工具
  • VoiceOver / NVDA:用屏幕阅读器实际体验

在 CI 里加 axe-core 做自动化检查:

import axe from '@axe-core/react';

it('should not have accessibility violations', async () => {
  const { container } = render(<App />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

无障碍不是一个额外的需求,而是产品的基本质量要求。从语义化 HTML 开始,养成好习惯,大部分问题可以预防。

ReactCSS
返回首页