第一章:为什么无障碍至关重要?
1.1 用户规模与法律风险
- 全球残障人士 :超 13 亿人(WHO),占人口 16%
- 中国视障用户 :超 1700 万 ,听障用户 2700 万
- 法律合规 :
- 欧盟 EN 301 549
- 美国 ADA / Section 508
- 中国《无障碍环境建设法》(2023 实施)
案例:Domino's Pizza 因网站不支持屏幕阅读器被起诉并败诉。
1.2 无障碍 = 更好的用户体验
- 键盘用户:开发者、游戏玩家、临时手部受伤者
- 高对比度模式:强光下户外用户受益
- 字幕/文字替代:嘈杂环境中的视频观看者
无障碍优化往往带来 SEO、移动端体验、性能的同步提升。
第二章:WCAG 2.2 核心原则(POUR)
| 原则 | 含义 | 关键成功标准(AA 级) |
|---|---|---|
| P - 可感知 | 信息可被用户感官识别 | 1.4.3 对比度 ≥ 4.5:1 1.2.2 预录音视频需字幕 |
| O - 可操作 | 组件可被交互 | 2.1.1 所有功能支持键盘 2.4.7 可见焦点指示 |
| U - 可理解 | 内容清晰可读 | 3.1.1 页面语言声明 3.2.4 一致标识 |
| R - 健壮性 | 兼容辅助技术 | 4.1.2 名称、角色、值可编程确定 |
本篇重点落地 AA 级中最常被忽视的 10 项标准。
第三章:语义化 HTML 与 ARIA
3.1 优先使用原生语义元素
错误做法 (仅用 <div>):
<!-- bad -->
<div @click="submit">提交</div>
正确做法 (使用 <button>):
<!-- good -->
<button @click="submit">提交</button>
原生元素自动具备:
- 键盘可聚焦(Tab 键)
- 屏幕阅读器识别为"按钮"
- 默认焦点样式
3.2 ARIA 的正确使用场景
ARIA(Accessible Rich Internet Applications)用于 增强语义,而非替代 HTML。
场景 1:动态区域通知
<template>
<!-- 操作结果实时通知屏幕阅读器 -->
<div aria-live="polite" class="sr-only">
{{ message }}
</div>
<button @click="deleteItem">删除</button>
</template>
<script setup>
const message = ref('')
const deleteItem = async () => {
await api.delete()
message.value = '项目已删除' // 屏幕阅读器自动朗读
}
</script>
场景 2:自定义组件角色
<!-- 自定义开关组件 -->
<template>
<div
role="switch"
:aria-checked="isChecked"
@click="toggle"
tabindex="0"
>
{{ isChecked ? '开' : '关' }}
</div>
</template>
ARIA 黄金法则:
- Don't use ARIA unless you must
- Test with real assistive tech
第四章:键盘导航全覆盖
4.1 焦点管理三原则
- 可聚焦:所有交互元素可通过 Tab 访问
- 可见焦点 :
:focus-visible样式清晰 - 逻辑顺序:DOM 顺序 = 视觉顺序
修复焦点丢失(Vue 动态内容)
<template>
<div ref="modalRef" role="dialog" aria-modal="true">
<button @click="close">关闭</button>
</div>
</template>
<script setup>
const modalRef = ref<HTMLElement | null>(null)
onMounted(() => {
// 模态框打开时,自动聚焦第一个可交互元素
modalRef.value?.querySelector('button')?.focus()
// 禁止背景滚动(避免焦点逃逸)
document.body.style.overflow = 'hidden'
})
onUnmounted(() => {
document.body.style.overflow = ''
})
</script>
4.2 跳转链接(Skip Link)
在页面顶部添加"跳至主内容"链接:
<!-- base.html (Flask Jinja2) -->
<body>
<a href="#main-content" class="skip-link">跳至主内容</a>
<header>...</header>
<main id="main-content">...</main>
</body>
/* 默认隐藏,聚焦时显示 */
.skip-link {
position: absolute;
top: -40px;
left: 6px;
background: #000;
color: #fff;
padding: 8px;
z-index: 1000;
}
.skip-link:focus {
top: 6px;
}
第五章:表单无障碍
5.1 标签与字段显式关联
错误:
<!-- bad: 无关联 -->
<div>用户名</div>
<input type="text">
正确:
<!-- good: 使用 for/id 或嵌套 -->
<template>
<label for="username">用户名</label>
<input id="username" type="text" v-model="username" required>
<!-- 或嵌套方式 -->
<label>
密码
<input type="password" v-model="password" required>
</label>
</template>
5.2 错误提示可访问
<template>
<label for="email">邮箱</label>
<input
id="email"
type="email"
v-model="email"
aria-invalid="true"
aria-describedby="email-error"
>
<div id="email-error" class="error" role="alert">
请输入有效邮箱地址
</div>
</template>
关键属性:
aria-invalid="true":标记无效字段aria-describedby:关联错误描述role="alert":立即通知屏幕阅读器
第六章:色彩与视觉设计
6.1 色彩对比度 ≥ 4.5:1
使用工具检测:
- WebAIM Contrast Checker
- VS Code 插件:Color Highlight
CSS 自定义属性保障:
:root {
--text-primary: #2d2d2d; /* 对比度 12:1 on white */
--text-secondary: #666666; /* 对比度 4.7:1 on white */
--bg-primary: #ffffff;
}
6.2 不依赖颜色传递信息
错误:
<!-- bad: 仅用红色表示错误 -->
<span style="color: red">库存不足</span>
正确:
<!-- good: 图标 + 文字 -->
<span aria-label="错误:库存不足">
⚠️ 库存不足
</span>
6.3 支持系统偏好(减少动画)
/* 尊重用户"减少动画"设置 */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
第七章:屏幕阅读器测试实战
7.1 免费工具清单
| 平台 | 屏幕阅读器 | 快捷键 |
|---|---|---|
| Windows | NVDA(免费) | CapsLock + 方向键 |
| macOS | VoiceOver(内置) | Cmd + F5 |
| iOS | VoiceOver | 三击侧边键 |
| Android | TalkBack | 三指滑动 |
7.2 测试清单
- 能否通过 Tab 访问所有控件?
- 屏幕阅读器是否正确朗读按钮/链接用途?
- 动态内容更新是否被通知?
- 表单错误是否即时播报?
- 模态框是否形成焦点陷阱?
技巧:闭眼操作 10 分钟,感受真实体验。
第八章:自动化无障碍测试
8.1 Cypress + axe-core 集成
// cypress/e2e/a11y.cy.ts
import { injectAxe, checkA11y } from 'cypress-axe'
describe('Accessibility Tests', () => {
beforeEach(() => {
cy.visit('/login')
injectAxe()
})
it('Should have no detectable a11y violations', () => {
cy.checkA11y(
null,
{
includedImpacts: ['critical', 'serious'], // 只检查严重问题
rules: {
'color-contrast': { enabled: true },
'button-name': { enabled: true }
}
}
)
})
})
8.2 CI 中阻断严重问题
# .github/workflows/a11y.yml
name: Accessibility Check
on: [pull_request]
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Cypress a11y tests
run: npx cypress run --spec "cypress/e2e/a11y.cy.ts"
# 若发现 critical 问题,CI 失败
注意 :自动化工具只能覆盖 30~40% 问题,人工测试不可替代。
第九章:服务端渲染(Flask Jinja2)的 a11y
9.1 页面语言声明
<!-- base.html -->
<html lang="{{ g.locale or 'zh-CN' }}">
9.2 主要区域标记
<body>
<header role="banner">...</header>
<nav role="navigation">...</nav>
<main role="main" id="main-content">...</main>
<footer role="contentinfo">...</footer>
</body>
9.3 表格语义化
<table>
<caption>用户列表</caption>
<thead>
<tr>
<th scope="col">姓名</th>
<th scope="col">邮箱</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
</tr>
{% endfor %}
</tbody>
</table>
第十章:建立包容性开发文化
10.1 a11y 融入开发流程
| 阶段 | 行动 |
|---|---|
| 需求 | 明确 a11y 验收标准(如"支持键盘操作") |
| 设计 | 提供高对比度设计稿、焦点状态规范 |
| 开发 | 组件库内置 a11y(如 Element Plus 的 a11y 选项) |
| 测试 | 手动 + 自动化 a11y 测试 |
| 上线 | 发布 a11y 声明(Accessibility Statement) |
10.2 团队赋能
- 培训:组织 NVDA/VoiceOver 工作坊
- 工具:安装浏览器 a11y 插件(如 WAVE)
- 倡导:设立 "a11y champion" 角色
总结:技术的温度,在于包容
无障碍不是"为少数人做的额外工作",而是"为所有人构建更好产品的基础"。