DevToolBox免费
博客

Web无障碍指南:WCAG 2.2、ARIA、键盘导航、屏幕阅读器和测试

17 分钟阅读作者 DevToolBox
TL;DR: 以语义化 HTML 为基础,仅在原生元素不足时添加 ARIA,确保 4.5:1 颜色对比度,支持完整的键盘导航和可见焦点指示器,为所有表单输入添加标签,提供有意义的 alt 文本,并使用 axe-core 和真实屏幕阅读器进行测试。WCAG 2.2 AA 是大多数法律管辖区的基准。

关键要点

  • 语义化 HTML 免费提供 80% 的无障碍能力——先使用原生元素,再考虑 ARIA。
  • WCAG 2.2 引入了关于焦点外观、拖拽替代方案和目标尺寸的新成功标准。
  • 颜色对比度至少为 4.5:1(普通文本)和 3:1(大文本),AA 级别。
  • 每个交互元素必须支持键盘操作并有可见的焦点指示器。
  • 表单需要关联标签、清晰的错误消息和程序化的验证公告。
  • 使用自动化工具(axe、Lighthouse)和手动屏幕阅读器(NVDA、VoiceOver)进行测试。

Web 无障碍确保网站和应用程序对所有人可用,包括视觉、听觉、运动或认知障碍的用户。全球超过13亿人生活在某种形式的残障中,无障碍不仅是法律要求,也是道德义务和商业优势。本指南涵盖 WCAG 2.2 合规级别、语义化 HTML、ARIA 属性、键盘导航、屏幕阅读器优化和测试策略。

1. WCAG 2.2 指南与合规级别

WCAG 2.2 由 W3C 于 2023 年 10 月发布,是当前的 Web 无障碍标准。它在 WCAG 2.1 的基础上增加了九个新的成功标准,围绕四个原则组织:可感知、可操作、可理解和健壮。

WCAG 定义三个合规级别:A 级覆盖最低基线,AA 级是大多数组织的标准目标,AAA 级代表最高级别但不作为通用要求。

LevelCriteria CountTarget AudienceLegal Requirement
A30Minimum baselineRequired everywhere
AA24Standard targetADA, Section 508, EAA
AAA33Enhanced accessibilityNot generally required

WCAG 2.2 新增:焦点不被遮挡(AA)、拖拽动作替代(AA)、最小目标尺寸 24x24 像素(AA)、一致的帮助(A)、冗余输入(A)。

2. 语义化 HTML 元素与地标

语义化 HTML 是无障碍 Web 开发的基础。原生 HTML 元素携带隐式角色、状态和键盘行为。button 元素可聚焦、可通过 Enter 和 Space 激活——div 加 onclick 不具备这些行为。

Semantic vs Non-Semantic HTML

<!-- BAD: Non-semantic (inaccessible) -->
<div class="header">
  <div class="nav">
    <div onclick="navigate('/home')">Home</div>
    <div onclick="navigate('/about')">About</div>
  </div>
</div>
<div class="main">
  <div class="title">Page Title</div>
  <div class="btn" onclick="submit()">Submit</div>
</div>

<!-- GOOD: Semantic (accessible) -->
<header>
  <nav aria-label="Main navigation">
    <a href="/home">Home</a>
    <a href="/about">About</a>
  </nav>
</header>
<main>
  <h1>Page Title</h1>
  <button type="submit">Submit</button>
</main>

HTML5 地标元素直接映射到 ARIA 地标角色:header 对应 banner,nav 对应 navigation,main 对应 main,aside 对应 complementary,footer 对应 contentinfo。

HTML ElementARIA Landmark RolePurpose
<header>bannerSite-wide header content
<nav>navigationNavigation links
<main>mainPrimary page content
<aside>complementarySupporting content
<footer>contentinfoSite-wide footer content
<section>region (with label)Thematic grouping
<form>form (with label)User input form

标题层次传达文档结构。每页一个 h1,然后按顺序使用 h2 到 h6。67.5% 的屏幕阅读器用户使用标题作为主要导航方式。

3. ARIA 角色、状态和属性

ARIA 在原生 HTML 语义不足时提供角色、状态和属性。ARIA 第一规则:如果存在具有等效语义的原生 HTML 元素,就不要使用 ARIA。

ARIA Tab Interface Example

<!-- Accessible tab component with ARIA -->
<div role="tablist" aria-label="Product info">
  <button role="tab"
    id="tab-details"
    aria-selected="true"
    aria-controls="panel-details"
    tabindex="0">Details</button>
  <button role="tab"
    id="tab-reviews"
    aria-selected="false"
    aria-controls="panel-reviews"
    tabindex="-1">Reviews</button>
  <button role="tab"
    id="tab-shipping"
    aria-selected="false"
    aria-controls="panel-shipping"
    tabindex="-1">Shipping</button>
</div>

<div role="tabpanel"
  id="panel-details"
  aria-labelledby="tab-details"
  tabindex="0">
  <p>Product details content here.</p>
</div>

<!-- Keyboard: Arrow keys between tabs,
     Tab key moves into active panel -->

ARIA 角色分为六类:地标角色、部件角色、文档结构角色、实时区域角色、窗口角色和抽象角色。

ARIA 状态和属性传达动态信息:aria-expanded 指示折叠区域状态,aria-live 向屏幕阅读器宣告动态内容更新。

aria-live Regions

<!-- Status message (polite) -->
<div aria-live="polite" aria-atomic="true">
  3 results found for "accessibility"
</div>

<!-- Error alert (assertive) -->
<div role="alert">
  <!-- role="alert" implies assertive -->
  Payment failed. Check your card details.
</div>

<!-- Progress update -->
<div role="status">
  <!-- role="status" implies polite -->
  Uploading: 67% complete
</div>

4. 键盘导航与焦点管理

所有交互元素必须仅通过键盘即可操作。使用 tabindex 0 将元素添加到自然 Tab 序列。永远不要使用大于 0 的 tabindex 值。

KeyActionElement Type
TabMove to next focusable elementAll interactive
Shift + TabMove to previous elementAll interactive
EnterActivate link or buttonLinks, buttons
SpaceToggle button/checkboxButtons, checkboxes
EscapeClose dialog or dropdownModals, menus
Arrow keysNavigate within widgetTabs, menus, radios

焦点管理对单页应用至关重要。内容变化时需程序化移动焦点。使用 tabindex -1 使元素可编程聚焦。模态框需要焦点陷阱。

Focus Trap for Modal Dialog

function trapFocus(dialog) {
  const focusable = dialog.querySelectorAll(
    'button, [href], input, select,' +
    ' textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  dialog.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;

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

  first.focus();
}

WCAG 2.2 要求可见的焦点指示器,至少 2px 实线轮廓,3:1 对比度。考虑使用 :focus-visible 替代 :focus。

Focus Indicator CSS

/* Custom focus indicator (WCAG 2.2) */
:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
  border-radius: 2px;
}

/* High contrast for dark backgrounds */
.dark-section :focus-visible {
  outline: 2px solid #fbbf24;
  outline-offset: 2px;
}

5. 屏幕阅读器优化

主要屏幕阅读器:NVDA(Windows免费)、JAWS(商业)、VoiceOver(macOS/iOS内置)、TalkBack(Android内置)。

无障碍名称计算顺序:aria-labelledby > aria-label > label > 内容 > title > alt。确保每个交互元素都有无障碍名称。

Visually Hidden (Screen Reader Only)

.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;
}

/* Usage: visible to screen readers only */
<button>
  <svg aria-hidden="true"><!-- icon --></svg>
  <span class="sr-only">Close dialog</span>
</button>

实时区域在不移动焦点的情况下通知屏幕阅读器。polite 等待用户完成当前操作,assertive 立即中断。

6. 颜色对比度与视觉无障碍

AA 级要求普通文本 4.5:1、大文本 3:1 对比度。AAA 级提高到 7:1 和 4.5:1。非文本元素至少需要 3:1。

WCAG LevelNormal TextLarge TextUI Components
AA (required)4.5:13:13:1
AAA (enhanced)7:14.5:1N/A

不要仅依靠颜色传达信息。链接需要额外的视觉指示器。表单错误应结合颜色、图标和文本。

使用 CSS 媒体查询支持用户偏好:prefers-reduced-motion、prefers-contrast、prefers-color-scheme。

Respecting User Preferences

/* Reduce or disable animations */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Increase contrast when requested */
@media (prefers-contrast: more) {
  :root {
    --text-color: #000000;
    --bg-color: #ffffff;
    --border-color: #000000;
  }
}

/* Dark mode support */
@media (prefers-color-scheme: dark) {
  :root {
    --text-color: #f1f5f9;
    --bg-color: #0f172a;
  }
}

7. 表单无障碍

每个表单输入必须有程序化关联的标签。占位符文本不能替代标签。

Accessible Form Example

<form novalidate>
  <fieldset>
    <legend>Contact Information</legend>

    <label for="email">Email address</label>
    <input
      type="email"
      id="email"
      name="email"
      required
      aria-describedby="email-hint email-error"
      aria-invalid="false"
    />
    <span id="email-hint">
      We will never share your email.
    </span>
    <span id="email-error" role="alert"></span>
  </fieldset>

  <fieldset>
    <legend>Preferred contact method</legend>
    <label>
      <input type="radio" name="contact"
        value="email" /> Email
    </label>
    <label>
      <input type="radio" name="contact"
        value="phone" /> Phone
    </label>
  </fieldset>

  <button type="submit">Send message</button>
</form>

使用 fieldset 和 legend 对相关字段分组。使用 aria-describedby 链接说明文本。

无障碍错误处理:识别错误字段、描述错误、帮助用户修复。使用 aria-invalid 和 aria-live 通告错误。

Error Handling Pattern

function handleSubmit(form) {
  const errors = validate(form);
  if (errors.length === 0) return true;

  // Clear previous errors
  form.querySelectorAll('[aria-invalid]').forEach(
    el => el.setAttribute('aria-invalid', 'false')
  );

  // Mark fields with errors
  errors.forEach(({ fieldId, message }) => {
    const field = document.getElementById(fieldId);
    const errorEl = document.getElementById(
      fieldId + '-error'
    );
    field.setAttribute('aria-invalid', 'true');
    errorEl.textContent = message;
  });

  // Focus first error field
  document.getElementById(
    errors[0].fieldId
  ).focus();
  return false;
}

8. 图片 Alt 文本最佳实践

所有图片必须有 alt 属性。信息性图片需要描述性文本,装饰性图片使用空 alt。

描述内容和功能而非外观。保持简洁(通常不超过125个字符)。

Image TypeAlt Text ApproachExample
InformativeDescribe content/functionalt="Bar chart: 40% Q4 revenue increase"
DecorativeEmpty alt attributealt=""
Linked imageDescribe destinationalt="Company Name - Homepage"
Icon buttonDescribe actionalt="Search" (not "Magnifying glass")
Complex chartBrief summary + long descalt="Q4 sales" + aria-describedby
Text in imageReproduce the textalt="50% OFF Summer Sale"

图标按钮描述操作而非图标:搜索按钮的 alt 应为"搜索"而非"放大镜"。

<!-- SVG icon with accessibility -->
<button>
  <svg role="img" aria-label="Delete item"
    viewBox="0 0 24 24">
    <title>Delete item</title>
    <path d="M6 19c0 1.1.9 2 2 2h8..." />
  </svg>
</button>

<!-- Decorative SVG (hidden from AT) -->
<svg aria-hidden="true" focusable="false">
  <use href="#decorative-divider" />
</svg>

9. 响应式与移动端无障碍

WCAG 2.2 要求内容在 320px 宽度下无需水平滚动。文本可放大至 200%。不要禁用用户缩放。

触摸目标至少 44x44 像素,最低 24x24。复杂手势需提供简单替代方案。

<!-- BAD: Prevents user from zooming -->
<meta name="viewport"
  content="width=device-width,
  initial-scale=1, maximum-scale=1,
  user-scalable=no" />

<!-- GOOD: Allows user zoom -->
<meta name="viewport"
  content="width=device-width,
  initial-scale=1" />

/* Touch target sizing */
.touch-target {
  min-width: 44px;
  min-height: 44px;
  padding: 12px;
}

/* Ensure text reflows at 320px */
.content {
  max-width: 100%;
  overflow-wrap: break-word;
}

移动屏幕阅读器使用触摸手势。VoiceOver 用户左右滑动导航、双击激活。确保自定义组件支持这些手势。

10. 无障碍表格与数据可视化

数据表格需要正确的表头标记。使用 th 加 scope 属性。为表格添加 caption 或 aria-label。

Accessible Data Table

<table>
  <caption>
    Quarterly Revenue (USD millions)
  </caption>
  <thead>
    <tr>
      <th scope="col">Quarter</th>
      <th scope="col">Revenue</th>
      <th scope="col">Growth</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Q1 2025</th>
      <td>12.4</td>
      <td>+8%</td>
    </tr>
    <tr>
      <th scope="row">Q2 2025</th>
      <td>14.1</td>
      <td>+13.7%</td>
    </tr>
  </tbody>
</table>

不要用表格布局。图表和可视化需提供文本替代。

交互图表需要键盘导航和无障碍工具提示。考虑使用 aria-live 在实时仪表板中通告数据变化。

11. 使用 axe、Lighthouse 和屏幕阅读器测试

自动化测试发现 30-50% 的问题。使用 axe-core 作为主要工具。Lighthouse 提供快速概览。

Automated Testing with axe-core

// axe-core in Cypress E2E tests
describe('Accessibility', () => {
  it('has no a11y violations', () => {
    cy.visit('/');
    cy.injectAxe();
    cy.checkA11y(null, {
      runOnly: {
        type: 'tag',
        values: ['wcag2a', 'wcag2aa',
          'wcag22aa']
      }
    });
  });
});

// axe-core in Jest unit tests
import { axe, toHaveNoViolations }
  from 'jest-axe';
expect.extend(toHaveNoViolations);

test('form is accessible', async () => {
  const { container } = render(<LoginForm />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

手动测试:仅用 Tab、Enter、Space、Escape 和方向键。至少使用一个屏幕阅读器测试。

在 CI 管道中运行 axe 检查。在用户故事中包含无障碍验收标准。定期与使用辅助技术的真实用户一起审计。

ToolTypePlatformBest For
axe DevToolsExtensionChrome/FirefoxQuick page audits
axe-coreCI/CD libNode.jsRegression testing
LighthouseBuilt-inChrome DevToolsQuick audit scores
NVDAScreen readerWindows (free)Manual SR testing
VoiceOverScreen readermacOS/iOSMac/mobile testing
pa11yCLI toolNode.jsCI pipeline

12. 常见无障碍反模式

用 div/span 做交互元素是最常见的反模式。解决方案:使用原生 button 和 a 元素。

Common Mistakes & Fixes

<!-- Anti-pattern: clickable div -->
<div class="btn" onclick="save()">
  Save
</div>
<!-- Fix: use native button -->
<button type="button" onclick="save()">
  Save
</button>

<!-- Anti-pattern: missing label -->
<input type="text" placeholder="Search..." />
<!-- Fix: add associated label -->
<label for="search">Search</label>
<input type="text" id="search"
  placeholder="Search..." />

<!-- Anti-pattern: redundant ARIA -->
<button role="button"
  aria-label="Submit button">
  Submit
</button>
<!-- Fix: remove unnecessary ARIA -->
<button type="submit">Submit</button>

移除焦点轮廓而不提供替代;自动播放有声媒体;使用大于 0 的 tabindex;用 display:none 隐藏屏幕阅读器需要的内容。

ARIA 过度使用:为每个元素添加 aria-label、在 button 上添加冗余的 role=button、对可见元素使用 aria-hidden=true。

13. 法律要求(ADA、508条款、EAA)

美国 ADA 第三条适用于网站。508 条款要求联邦机构遵循 WCAG 2.0 AA。2024年DOJ规则要求州和地方政府遵循 WCAG 2.1 AA。

欧洲无障碍法案(EAA)2025年6月生效,要求网站和移动应用无障碍。EN 301 549 对应 WCAG 2.1 AA。

RegulationRegionStandardEffective
ADA Title IIIUnited StatesWCAG 2.1 AAActive
Section 508US FederalWCAG 2.0 AAActive
EAAEuropean UnionWCAG 2.1 AAJune 2025
AODAOntario, CanadaWCAG 2.0 AAActive
DDAAustraliaWCAG 2.0 AAActive

2023年美国提起超过4000起数字无障碍诉讼。主动合规远比被动补救便宜。WCAG 2.1 AA 是全球合规的实际目标。

总结

Web 无障碍是持续的实践,而非一次性清单。从语义化 HTML 开始,仅在需要时添加 ARIA,确保键盘和屏幕阅读器兼容性,保持颜色对比标准,并定期测试。WCAG 2.2 AA 应是最低目标。无障碍网站不仅合法合规,而且更快、更易用、覆盖更广泛的受众。

常见问题

WCAG 2.1 和 2.2 有什么区别?

WCAG 2.2 增加了九个新成功标准,包括焦点不被遮挡、拖拽动作替代和最小目标尺寸。满足 2.2 即满足 2.1 和 2.0。

ARIA 比语义化 HTML 更好吗?

不。语义化 HTML 始终是首选。ARIA 在原生元素不足时作为补充。第一规则是如果存在等效的原生元素就不要使用 ARIA。

最低颜色对比度要求是多少?

AA 级要求普通文本 4.5:1、大文本 3:1。AAA 级要求 7:1 和 4.5:1。非文本 UI 组件至少 3:1。

我需要用屏幕阅读器测试吗?

需要。自动化工具仅发现 30-50% 的问题。使用 VoiceOver(macOS)或 NVDA(Windows)进行手动测试。

可以用 tabindex 控制 Tab 顺序吗?

使用 tabindex=0 和 tabindex=-1。永远不要使用正值 tabindex,通过调整 DOM 顺序来修复 Tab 顺序。

如何让单页应用无障碍?

在路由变化时管理焦点,使用 aria-live 通告路由变化,确保浏览器返回按钮工作,每次路由更新文档标题。

网站不无障碍有什么法律后果?

在美国,ADA 诉讼可能导致数万美元赔偿加律师费和强制补救。在欧洲,EAA 可处以罚款。不可访问的网站会损失 15-20% 的潜在客户。

如何处理动态内容和 AJAX 更新的无障碍?

使用 aria-live 区域通告动态内容变化。非紧急使用 polite,紧急警报使用 assertive。在内容变化前添加 aria-live 属性。

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

.*Regex Tester

相关文章

CSS架构指南:BEM、CSS模块、CSS-in-JS、Tailwind、自定义属性和暗色模式

掌握大规模应用的CSS架构。涵盖BEM/SMACSS/OOCSS方法论、React中的CSS模块、styled-components vs emotion vs vanilla-extract、Tailwind工具类优先、CSS自定义属性主题化、Grid vs Flexbox、容器查询以及暗色模式。

React Hooks 完全指南:useState、useEffect 和自定义 Hooks

通过实际示例掌握 React Hooks。学习 useState、useEffect、useContext、useReducer、useMemo、useCallback、自定义 Hooks 和 React 18+ 并发 Hooks。

React测试指南:React Testing Library、Jest、Vitest、MSW、Playwright和代码覆盖率

掌握React测试从单元测试到端到端测试。涵盖RTL查询、userEvent、renderHook、jest.mock()、Mock Service Worker、Vitest、异步测试、快照测试、Redux/Zustand测试以及Playwright vs Cypress对比。