全球超过 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 最高:
- 语义化 HTML:零成本,解决 80% 的无障碍问题
- 键盘可操作:覆盖视障和运动障碍用户,同时也是 power user 的需求
- CI/CD 自动化:axe-core + Playwright 一行配置,持续守护无障碍质量
⚡ **关键结论:**不要追求一次性做到 AAA 级——先确保核心页面通过 AA 级标准,然后逐步改进。无障碍是一个持续改进的过程,而不是一个可以「完成」的里程碑。
相关工具推荐:
- 🔧 axe DevTools — 浏览器扩展,实时检测无障碍问题
- 🔧 WAVE — 可视化无障碍评估工具
- 🔧 Color Contrast Analyzer — 颜色对比度检测
- 🔧 Accessible Colors — 快速检查配色方案的可访问性
- 🔧 WAI-ARIA Authoring Practices — 复杂组件的官方实现指南