Web 无障碍开发实战指南:WCAG 2.2、ARIA 与前端可访问性完全攻略

深入解析 Web 无障碍(Accessibility/a11y)开发核心标准与实战技巧,涵盖 WCAG 2.2 四大原则、ARIA 角色与状态、键盘导航、焦点管理、颜色对比度、屏幕阅读器适配,附完整可运行代码示例与自动化测试方案。

前端开发 2026-05-30 15 分钟

全球超过 10 亿人存在某种形式的残障,占世界人口的 15%——而根据 WebAIM 2025 年对 Top 100 万网站首页的检测,96.3% 的页面存在可检测到的无障碍错误。欧盟《无障碍法案》(European Accessibility Act)已于 2025 年 6 月正式生效,不合规的企业面临最高营业额 5% 的罚款。Web 无障碍(Accessibility,简称 a11y)不是「锦上添花」的功能增强,而是每一个专业前端开发者必须掌握的核心工程能力。本文将从标准解读到代码实战,系统性地讲解如何构建真正可访问的 Web 应用。

🎯 一、WCAG 2.2 核心原则与合规要求

1.1 POUR 四大原则

WCAG(Web Content Accessibility Guidelines)是 W3C 制定的国际标准,当前最新版本为 WCAG 2.2(2023 年 10 月发布)。整个标准建立在四大原则之上:

原则 英文 含义 典型要求
可感知 Perceivable 信息必须以用户能感知的方式呈现 文本替代、字幕、颜色对比度
可操作 Operable 界面组件必须可操作 键盘可访问、足够的操作时间
可理解 Understandable 信息和操作必须可理解 语言标识、一致的导航、错误提示
健壮性 Robust 内容必须足够健壮以兼容辅助技术 有效的语义化 HTML、ARIA

📌 **记住:**这四个原则(POUR)是所有无障碍需求的根基。当你不确定某个设计决策是否正确时,回到这四个原则来判断。

1.2 合规等级与法规要求

WCAG 定义了三个合规等级:

  • A 级:最基本的无障碍要求,不满足会导致部分用户完全无法使用
  • AA 级:行业公认的标准线,大部分法规要求达到此级别
  • AAA 级:最高级别,适用于特殊场景(如政府、医疗)

全球主要法规对照:

法规 地区 要求等级 生效时间 罚款上限
European Accessibility Act 欧盟 WCAG 2.1 AA 2025-06-28 营业额 5%
ADA Title III 美国 WCAG 2.1 AA(事实标准) 持续有效 每次违规 $75,000+
无障碍设计规范 GB/T 37668 中国 参考 WCAG 2.0 AA 2019 年 行政处罚

⚡ **关键结论:**如果你的产品面向国际市场,WCAG 2.1 AA 是最低合规标准;面向国内市场,GB/T 37668 是参考依据。

1.3 WCAG 2.2 新增的关键标准

WCAG 2.2 相比 2.1 新增了 9 个成功标准,其中对前端开发者影响最大的三个:

  • 2.4.11 Focus Not Obscured (AA):获得焦点的元素不能被其他内容完全遮挡
  • 2.4.13 Focus Appearance (AAA):焦点指示器必须有足够的面积和对比度
  • 3.2.6 Consistent Help (A):帮助机制在多页应用中必须保持一致位置

🔧 二、前端无障碍开发实战

2.1 语义化 HTML:无障碍的基石

语义化 HTML 是无障碍开发中投入产出比最高的实践。正确使用 HTML 语义元素,可以在不写一行 ARIA 的情况下解决 80% 的无障碍问题。

❌ **错误写法:**用 div 模拟按钮

<!-- ❌ 错误:屏幕阅读器无法识别这是一个按钮 -->
<div class="btn" onclick="submit()">提交</div>

✅ **正确写法:**使用语义化 button 元素

<!-- ✅ 正确:自带键盘支持、角色识别、焦点管理 -->
<button type="submit">提交</button>

核心语义化元素对照:

用途 ❌ 非语义写法 ✅ 语义化写法 无障碍收益
导航 <div class="nav"> <nav> 屏幕阅读器可直接跳转
主要内容 <div id="main"> <main> 一键跳过导航栏
标题层级 <span class="title"> <h1>~<h6> 支持标题导航
列表 <div class="list"> <ul>/<ol> 读出列表项数量
表单标签 <span>用户名</span> <label for="name"> 点击标签聚焦输入框
地标 <div class="footer"> <footer> 屏幕阅读器地标导航

⚠️ **警告:**永远不要为了视觉效果而牺牲语义。视觉样式应该通过 CSS 实现,HTML 结构应该反映内容的真实含义。

2.2 ARIA 角色、状态与属性

当原生 HTML 语义不足以表达复杂交互时,WAI-ARIA(Web Accessibility Initiative – Accessible Rich Internet Applications)提供了补充手段。但记住 WAI-ARIA 的第一规则:

📌 **记住:**如果你能使用原生 HTML 元素或属性,并且具有你所需的行为和语义,那么不要使用 WAI-ARIA。

以下是前端开发中最常用的 ARIA 属性实战:

// ✅ 自定义下拉菜单的 ARIA 实现
function initAccessibleDropdown(trigger, listbox) {
  // 设置触发器的 ARIA 属性
  trigger.setAttribute('role', 'combobox')
  trigger.setAttribute('aria-expanded', 'false')
  trigger.setAttribute('aria-haspopup', 'listbox')
  trigger.setAttribute('aria-controls', listbox.id)

  // 设置列表框的 ARIA 属性
  listbox.setAttribute('role', 'listbox')

  // 为每个选项设置 role
  const options = listbox.querySelectorAll('[role="option"]')
  options.forEach((option, index) => {
    option.setAttribute('id', `option-${index}`)
    option.setAttribute('aria-selected', 'false')
    option.setAttribute('tabindex', '-1')
  })

  let currentIndex = -1

  // 切换展开/折叠状态
  function toggle(expanded) {
    trigger.setAttribute('aria-expanded', String(expanded))
    listbox.hidden = !expanded
    if (expanded && currentIndex >= 0) {
      options[currentIndex].focus()
    }
  }

  // 键盘导航
  trigger.addEventListener('keydown', (e) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault()
        toggle(true)
        currentIndex = 0
        updateSelection()
        break
      case 'Escape':
        toggle(false)
        trigger.focus()
        break
    }
  })

  listbox.addEventListener('keydown', (e) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault()
        currentIndex = Math.min(currentIndex + 1, options.length - 1)
        updateSelection()
        break
      case 'ArrowUp':
        e.preventDefault()
        currentIndex = Math.max(currentIndex - 1, 0)
        updateSelection()
        break
      case 'Enter':
      case ' ':
        e.preventDefault()
        selectOption(currentIndex)
        break
      case 'Escape':
        toggle(false)
        trigger.focus()
        break
    }
  })

  function updateSelection() {
    options.forEach((opt, i) => {
      opt.setAttribute('aria-selected', i === currentIndex ? 'true' : 'false')
      opt.setAttribute('tabindex', i === currentIndex ? '0' : '-1')
    })
    options[currentIndex]?.focus()
    trigger.setAttribute('aria-activedescendant', options[currentIndex]?.id || '')
  }

  function selectOption(index) {
    trigger.textContent = options[index].textContent
    trigger.setAttribute('aria-expanded', 'false')
    listbox.hidden = true
    trigger.focus()
    // 触发自定义事件
    trigger.dispatchEvent(new CustomEvent('change', {
      detail: { value: options[index].dataset.value }
    }))
  }
}

最常用的 ARIA 属性速查表:

属性 用途 示例
aria-label 为无文本元素提供标签 <button aria-label="关闭对话框">×</button>
aria-labelledby 引用其他元素作为标签 <div aria-labelledby="title-1">
aria-describedby 提供额外描述 <input aria-describedby="hint-1">
aria-expanded 表示展开/折叠状态 aria-expanded="true"
aria-hidden 对辅助技术隐藏元素 装饰性图标用 aria-hidden="true"
aria-live 声明动态更新区域 aria-live="polite" 用于 Toast
aria-required 表示必填字段 aria-required="true"
aria-invalid 表示验证错误 aria-invalid="true"

2.3 焦点管理:键盘无障碍的核心

焦点管理是无障碍开发中最容易被忽视、也最容易出错的环节。一个专业的 Web 应用必须保证纯键盘用户可以完成所有操作。

// ✅ 模态对话框的完整焦点管理
class AccessibleDialog {
  constructor(dialogElement) {
    this.dialog = dialogElement
    this.previouslyFocused = null
    this.focusableSelectors = [
      'a[href]', 'button:not([disabled])', 'input:not([disabled])',
      'select:not([disabled])', 'textarea:not([disabled])',
      '[tabindex]:not([tabindex="-1"])', 'details', 'summary',
      'video[controls]', 'audio[controls]', '[contenteditable]'
    ].join(',')
  }

  open() {
    // 1. 记住当前焦点位置
    this.previouslyFocused = document.activeElement

    // 2. 显示对话框
    this.dialog.hidden = false
    this.dialog.setAttribute('role', 'dialog')
    this.dialog.setAttribute('aria-modal', 'true')

    // 3. 将焦点移到对话框内的第一个可聚焦元素
    const focusable = this.dialog.querySelectorAll(this.focusableSelectors)
    if (focusable.length > 0) {
      focusable[0].focus()
    } else {
      this.dialog.focus()
    }

    // 4. 设置焦点陷阱
    this.dialog.addEventListener('keydown', this._trapFocus)

    // 5. ESC 关闭
    this.dialog.addEventListener('keydown', this._handleEscape)
  }

  close() {
    this.dialog.hidden = true
    this.dialog.removeEventListener('keydown', this._trapFocus)
    this.dialog.removeEventListener('keydown', this._handleEscape)

    // 恢复之前的焦点
    if (this.previouslyFocused) {
      this.previouslyFocused.focus()
    }
  }

  _trapFocus = (e) => {
    if (e.key !== 'Tab') return

    const focusable = this.dialog.querySelectorAll(this.focusableSelectors)
    const firstFocusable = focusable[0]
    const lastFocusable = focusable[focusable.length - 1]

    if (e.shiftKey) {
      // Shift + Tab: 在第一个元素上按 Tab,跳到最后一个
      if (document.activeElement === firstFocusable) {
        e.preventDefault()
        lastFocusable.focus()
      }
    } else {
      // Tab: 在最后一个元素上按 Tab,跳到第一个
      if (document.activeElement === lastFocusable) {
        e.preventDefault()
        firstFocusable.focus()
      }
    }
  }

  _handleEscape = (e) => {
    if (e.key === 'Escape') {
      e.preventDefault()
      this.close()
    }
  }
}

焦点管理的三条黄金法则:

  • 打开模态框时:焦点移入模态框,设置焦点陷阱
  • 关闭模态框时:焦点恢复到触发元素
  • 动态内容变化时:将焦点移到新内容或使用 aria-live 通知

⚠️ **警告:**永远不要使用 tabindex 值大于 0。正值 tabindex 会创建一个全局的焦点顺序,导致维护噩梦。只使用 tabindex="0"(可聚焦)和 tabindex="-1"(编程聚焦)。

2.4 颜色对比度与视觉无障碍

颜色对比度不足是 WebAIM 报告中最常见的无障碍错误。WCAG 2.2 要求:

场景 AA 级要求 AAA 级要求
普通文本(< 18px) 对比度 ≥ 4.5:1 对比度 ≥ 7:1
大文本(≥ 18px 或 14px 加粗) 对比度 ≥ 3:1 对比度 ≥ 4.5:1
UI 组件和图形 对比度 ≥ 3:1
// ✅ 颜色对比度计算工具
function getContrastRatio(hex1, hex2) {
  function hexToRgb(hex) {
    hex = hex.replace('#', '')
    return {
      r: parseInt(hex.substring(0, 2), 16),
      g: parseInt(hex.substring(2, 4), 16),
      b: parseInt(hex.substring(4, 6), 16)
    }
  }

  function relativeLuminance({ r, g, b }) {
    const [rs, gs, bs] = [r, g, b].map(c => {
      c = c / 255
      return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
    })
    return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs
  }

  const lum1 = relativeLuminance(hexToRgb(hex1))
  const lum2 = relativeLuminance(hexToRgb(hex2))
  const lighter = Math.max(lum1, lum2)
  const darker = Math.min(lum1, lum2)

  return (lighter + 0.05) / (darker + 0.05)
}

// 使用示例
const ratio = getContrastRatio('#2563eb', '#ffffff')
console.log(`对比度: ${ratio.toFixed(2)}:1`)
// 输出: 对比度: 4.61:1 ✅ 通过 AA 级

const badRatio = getContrastRatio('#aaaaaa', '#ffffff')
console.log(`对比度: ${badRatio.toFixed(2)}:1`)
// 输出: 对比度: 2.32:1 ❌ 不通过

⚠️ **关键提醒:**不要仅靠颜色传递信息。色盲用户约占男性人口的 8%,确保所有颜色编码的信息都有文本或图标替代。

💡 三、高级无障碍模式与实战案例

3.1 动态内容与 Live Region

当页面内容动态更新(如 Toast 通知、实时数据、表单验证错误)时,屏幕阅读器不会自动感知变化。aria-live 属性可以解决这个问题。

// ✅ 无障碍 Toast 通知系统
class AccessibleToast {
  constructor() {
    // 创建 live region 容器
    this.container = document.createElement('div')
    this.container.setAttribute('role', 'status')
    this.container.setAttribute('aria-live', 'polite')
    this.container.setAttribute('aria-atomic', 'true')
    this.container.classList.add('toast-container')

    // 视觉上隐藏但屏幕阅读器可读
    this.container.style.cssText = `
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border: 0;
    `
    document.body.appendChild(this.container)
  }

  show(message, type = 'info') {
    // 创建可见的 Toast
    const toast = document.createElement('div')
    toast.classList.add('toast', `toast-${type}`)
    toast.textContent = message

    // 同时更新 live region(屏幕阅读器会朗读)
    this.container.textContent = message

    // 可见 Toast 用 role="alert" 表示紧急(错误类型)
    if (type === 'error') {
      toast.setAttribute('role', 'alert')
      toast.setAttribute('aria-live', 'assertive')
    }

    document.body.appendChild(toast)

    // 3 秒后移除
    setTimeout(() => {
      toast.remove()
      this.container.textContent = ''
    }, 3000)
  }
}

// 使用
const toast = new AccessibleToast()
toast.show('保存成功', 'success')
toast.show('网络连接失败,请重试', 'error')

aria-live 的三种取值:

行为 适用场景
off 不播报更新 默认值,装饰性内容
polite 等待当前播报结束后播报 Toast 通知、搜索结果更新
assertive 立即打断当前播报 错误消息、紧急通知

⚠️ 警告:aria-live="assertive" 应该极少使用。滥用会导致屏幕阅读器用户体验极差。只有真正紧急的错误才用 assertive。

3.2 表单无障碍完整实现

表单是无障碍问题的高发区。一个无障碍表单需要:正确的标签关联、清晰的错误提示、必填标识、输入格式说明。

// ✅ 无障碍表单验证系统
class AccessibleForm {
  constructor(formElement) {
    this.form = formElement
    this.errors = new Map()
    this.form.setAttribute('novalidate', '')
    this.form.addEventListener('submit', this.handleSubmit.bind(this))
  }

  // 添加字段验证规则
  addField(name, rules) {
    const field = this.form.querySelector(`[name="${name}"]`)
    if (!field) return

    // 关联 label
    const label = this.form.querySelector(`label[for="${field.id}"]`)
    if (!label) {
      console.warn(`字段 ${name} 缺少关联的 <label>`)
    }

    // 创建错误提示元素
    const errorId = `${field.id}-error`
    const errorEl = document.createElement('span')
    errorEl.id = errorId
    errorEl.setAttribute('role', 'alert')
    errorEl.setAttribute('aria-live', 'polite')
    errorEl.classList.add('field-error')
    field.parentNode.appendChild(errorEl)

    // 关联错误提示
    field.setAttribute('aria-describedby', errorId)

    // 实时验证(失焦时)
    field.addEventListener('blur', () => {
      this.validateField(name, rules)
    })
  }

  validateField(name, rules) {
    const field = this.form.querySelector(`[name="${name}"]`)
    const errorEl = document.getElementById(`${field.id}-error`)
    const value = field.value.trim()

    for (const rule of rules) {
      if (rule.required && !value) {
        this.setFieldError(field, errorEl, rule.message || '此字段为必填项')
        return false
      }
      if (rule.pattern && !rule.pattern.test(value)) {
        this.setFieldError(field, errorEl, rule.message || '格式不正确')
        return false
      }
      if (rule.minLength && value.length < rule.minLength) {
        this.setFieldError(field, errorEl, `最少需要 ${rule.minLength} 个字符`)
        return false
      }
    }

    this.clearFieldError(field, errorEl)
    return true
  }

  setFieldError(field, errorEl, message) {
    field.setAttribute('aria-invalid', 'true')
    field.classList.add('is-invalid')
    errorEl.textContent = message
    this.errors.set(field.name, message)
  }

  clearFieldError(field, errorEl) {
    field.setAttribute('aria-invalid', 'false')
    field.classList.remove('is-invalid')
    errorEl.textContent = ''
    this.errors.delete(field.name)
  }

  handleSubmit(e) {
    e.preventDefault()
    const isValid = /* 验证所有字段 */ true
    if (!isValid) {
      // 将焦点移到第一个错误字段
      const firstInvalid = this.form.querySelector('[aria-invalid="true"]')
      if (firstInvalid) {
        firstInvalid.focus()
        // 汇总错误信息
        const errorCount = this.errors.size
        const summary = `表单有 ${errorCount} 个错误,请修正后重新提交`
        this.announceError(summary)
      }
    }
  }

  announceError(message) {
    // 创建一个临时的 alert 元素
    const alert = document.createElement('div')
    alert.setAttribute('role', 'alert')
    alert.setAttribute('aria-live', 'assertive')
    alert.classList.add('sr-only')
    alert.textContent = message
    document.body.appendChild(alert)
    setTimeout(() => alert.remove(), 1000)
  }
}

3.3 复杂组件无障碍模式

对于 Tab 切换、手风琴、Tree View 等复杂组件,WAI-ARIA Authoring Practices 提供了标准的键盘交互模式。

// ✅ 无障碍 Tab 切换组件
class AccessibleTabs {
  constructor(container) {
    this.container = container
    this.tablist = container.querySelector('[role="tablist"]')
    this.tabs = [...container.querySelectorAll('[role="tab"]')]
    this.panels = [...container.querySelectorAll('[role="tabpanel"]')]

    this.init()
  }

  init() {
    // 初始化 ARIA 属性
    this.tabs.forEach((tab, index) => {
      tab.setAttribute('tabindex', index === 0 ? '0' : '-1')
      tab.setAttribute('aria-selected', index === 0 ? 'true' : 'false')
      tab.setAttribute('aria-controls', this.panels[index].id)
      this.panels[index].setAttribute('aria-labelledby', tab.id)
      this.panels[index].setAttribute('tabindex', '0')

      // 如果不是第一个,隐藏面板
      if (index !== 0) {
        this.panels[index].hidden = true
      }
    })

    // 点击切换
    this.tabs.forEach((tab) => {
      tab.addEventListener('click', () => this.selectTab(tab))
    })

    // 键盘导航(箭头键在 Tab 间移动)
    this.tablist.addEventListener('keydown', (e) => {
      const currentIndex = this.tabs.indexOf(document.activeElement)
      if (currentIndex === -1) return

      let newIndex = currentIndex
      switch (e.key) {
        case 'ArrowRight':
          newIndex = (currentIndex + 1) % this.tabs.length
          break
        case 'ArrowLeft':
          newIndex = (currentIndex - 1 + this.tabs.length) % this.tabs.length
          break
        case 'Home':
          newIndex = 0
          break
        case 'End':
          newIndex = this.tabs.length - 1
          break
        default:
          return
      }

      e.preventDefault()
      this.selectTab(this.tabs[newIndex])
      this.tabs[newIndex].focus()
    })
  }

  selectTab(selectedTab) {
    // 取消所有选中
    this.tabs.forEach((tab, index) => {
      tab.setAttribute('aria-selected', 'false')
      tab.setAttribute('tabindex', '-1')
      this.panels[index].hidden = true
    })

    // 选中目标
    const index = this.tabs.indexOf(selectedTab)
    selectedTab.setAttribute('aria-selected', 'true')
    selectedTab.setAttribute('tabindex', '0')
    this.panels[index].hidden = false
  }
}

🧪 四、无障碍测试与自动化工具

4.1 工具对比与选型

工具 类型 集成方式 检测范围 推荐场景
axe-core 自动化 CLI / CI / 浏览器 约 57% 的 WCAG 问题 CI/CD 流水线
Lighthouse 自动化 Chrome DevTools / CLI 综合评分 开发阶段快速检查
pa11y 自动化 CLI / CI 底层 HTML 问题 批量页面扫描
WAVE 半自动 浏览器扩展 可视化标注 开发调试
NVDA 手动测试 Windows 桌面软件 屏幕阅读器体验 发布前回归测试
VoiceOver 手动测试 macOS/iOS 内置 屏幕阅读器体验 Mac/iOS 设备测试
# ✅ 使用 axe-core 在 CI 中检测无障碍问题
npm install --save-dev @axe-core/cli

# 检测单个页面
npx axe http://localhost:3000 --rules wcag2a,wcag2aa

# 检测多个页面(结合 sitemap)
npx axe http://localhost:3000 \
  http://localhost:3000/about \
  http://localhost:3000/contact \
  --tags wcag2a,wcag2aa,wcag22aa \
  --exit
// ✅ 在 Playwright E2E 测试中集成 axe-core
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'

test.describe('无障碍测试', () => {
  test('首页应无严重无障碍违规', async ({ page }) => {
    await page.goto('http://localhost:3000')

    const results = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
      .analyze()

    // 零违规
    expect(results.violations).toEqual([])
  })

  test('表单应有正确的标签关联', async ({ page }) => {
    await page.goto('http://localhost:3000/contact')

    const results = await new AxeBuilder({ page })
      .include('#contact-form')
      .withRules(['label', 'select-name'])
      .analyze()

    expect(results.violations).toEqual([])
  })
})

4.2 手动测试清单

自动化工具只能检测约 57% 的无障碍问题,其余必须通过手动测试验证:

键盘测试(每次发布必做):

  • ✅ Tab 键可以到达所有交互元素
  • ✅ Shift + Tab 可以反向导航
  • ✅ Enter/Space 可以激活按钮和链接
  • ✅ Arrow 键可以在 Tab、Menu、Radio 等组件内导航
  • ✅ Escape 可以关闭模态框和下拉菜单
  • ✅ 焦点指示器清晰可见(不是 outline: none
  • ✅ 焦点不会陷入死循环(如模态框的焦点陷阱)

屏幕阅读器测试(核心流程必做):

  • ✅ 所有图片有有意义的 alt 文本(装饰性图片用 alt=""
  • ✅ 表单字段有关联的标签
  • ✅ 动态内容更新时有 aria-live 通知
  • ✅ 页面标题准确描述当前页面
  • ✅ 地标区域(nav、main、footer)正确标记

✅ 五、无障碍开发最佳实践清单

HTML 结构:

  • ✅ 使用语义化 HTML 元素,避免 div soup
  • ✅ 标题层级(h1-h6)连续且有意义
  • ✅ 页面 <html> 标签设置 lang 属性
  • ✅ 每个页面有唯一的 <title>

交互组件:

  • ✅ 所有交互元素可通过键盘操作
  • ✅ 模态框实现焦点陷阱
  • ✅ 动态内容使用 aria-live 通知
  • ✅ 复杂组件遵循 WAI-ARIA 设计模式

视觉设计:

  • ✅ 文本颜色对比度 ≥ 4.5:1(AA 级)
  • ✅ 不仅靠颜色传递信息,同时使用文本/图标
  • ✅ 点击目标最小 44×44 像素(WCAG 2.2 建议)
  • ✅ 支持浏览器缩放到 200% 不丢失功能

测试流程:

  • ✅ CI/CD 中集成 axe-core 自动检测
  • ✅ 每次发布前做键盘导航测试
  • ✅ 核心流程用屏幕阅读器验证
  • ✅ 定期用 Lighthouse 扫描并跟踪分数

📊 总结

Web 无障碍不是一项「额外工作」,而是专业前端开发的基本素养。从投入产出比来看,以下三件事的 ROI 最高:

  1. 语义化 HTML:零成本,解决 80% 的无障碍问题
  2. 键盘可操作:覆盖视障和运动障碍用户,同时也是 power user 的需求
  3. CI/CD 自动化:axe-core + Playwright 一行配置,持续守护无障碍质量

⚡ **关键结论:**不要追求一次性做到 AAA 级——先确保核心页面通过 AA 级标准,然后逐步改进。无障碍是一个持续改进的过程,而不是一个可以「完成」的里程碑。

相关工具推荐:

📚 相关文章