线上无障碍问题如何整改
场景
产品收到客户投诉或合规审计报告:键盘无法完成核心流程、弹窗焦点丢失、表单错误读屏器不播报、拖拽功能没有替代方式。作为前端负责人,如何止血、整改并建立长期防线?
无障碍整改不能只跑一次 Lighthouse:
- 确认影响用户和完整任务路径,登录、支付、提交等阻断问题按线上故障处理;
- 用 WCAG 成功标准给问题编号和定级,保留浏览器、辅助技术和复现步骤;
- 先修语义和原生交互,再补 ARIA;优先键盘、焦点、名称/角色/状态和表单错误;
- 自动化扫描发现基础问题,人工键盘测试和真实读屏器验证行为;
- 为拖拽、悬停、颜色提示和超时流程提供等价替代;
- 修复进入组件库、Storybook、测试和 CI 门禁,避免页面逐个打补丁;
- 用任务成功率、阻断问题数和回归率衡量,而不只看自动化分数。
一、先做影响分级
| 等级 | 判断标准 | 示例 | 处理 |
|---|---|---|---|
| P0 | 核心任务完全无法完成且无替代 | 键盘无法登录或付款 | 立即止血和热修 |
| P1 | 关键功能严重受阻 | 弹窗焦点陷阱、错误不播报 | 当前迭代修复 |
| P2 | 体验明显下降但可绕过 | 焦点样式不清晰、标题层级混乱 | 排期治理 |
| P3 | 最佳实践和一致性问题 | 冗余播报、描述不够简洁 | 组件升级处理 |
严重程度应结合用户影响和任务链路,不应仅按 WCAG A、AA、AAA 机械映射。
二、建立可复现的审计记录
每个问题至少记录:
- URL、组件、操作步骤和期望结果;
- 浏览器、操作系统、缩放比例;
- 辅助技术及版本,例如 NVDA、VoiceOver;
- 对应 WCAG 成功标准;
- 截图、录屏或可访问性树证据;
- 负责人、修复版本和回归用例。
interface AccessibilityIssue {
id: string;
route: string;
component: string;
wcagCriterion: string;
severity: 'P0' | 'P1' | 'P2' | 'P3';
assistiveTechnology?: string;
steps: string[];
expected: string;
actual: string;
owner: string;
targetRelease: string;
}
三、整改顺序
1. 优先使用原生语义
原生 button、input、label、dialog 和标题结构通常已经提供键盘行为与可访问性语义。把 div 加上 role="button" 后,还需要自己实现焦点、Enter/Space、禁用状态等行为,维护成本更高。
2. 键盘和焦点
检查:
- 所有交互是否能用 Tab、Shift+Tab、Enter、Space、方向键完成;
- 焦点顺序是否符合视觉和任务顺序;
- 焦点样式是否清晰且未被 Sticky 区域遮挡;
- 弹窗打开后焦点是否进入,关闭后是否回到触发元素;
- 是否出现键盘陷阱或不可达控件。
import { useEffect, useRef } from 'react';
interface ConfirmDialogProps {
open: boolean;
title: string;
onConfirm: () => void;
onClose: () => void;
}
export function ConfirmDialog({
open,
title,
onConfirm,
onClose,
}: ConfirmDialogProps) {
const dialogRef = useRef<HTMLDialogElement>(null);
const triggerRef = useRef<HTMLElement | null>(null);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (open && !dialog.open) {
triggerRef.current = document.activeElement as HTMLElement | null;
dialog.showModal();
} else if (!open && dialog.open) {
dialog.close();
triggerRef.current?.focus();
}
}, [open]);
return (
<dialog ref={dialogRef} aria-labelledby="confirm-title" onCancel={onClose}>
<h2 id="confirm-title">{title}</h2>
<button type="button" onClick={onConfirm}>确认</button>
<button type="button" onClick={onClose}>取消</button>
</dialog>
);
}
复杂对话框仍要测试初始焦点、焦点循环、嵌套弹层和页面滚动;可以优先采用经过验证的无障碍组件原语。
3. 名称、角色和状态
- 每个控件有可感知且稳定的 Accessible Name;
- 展开、选中、忙碌、无效等状态可被程序识别;
- 图标按钮提供文本名称,但不重复播报装饰图标;
- 状态变化使用合适的 Live Region,不滥用
aria-live="assertive"; - ARIA 状态必须与真实视觉和交互状态同步。
错误的 ARIA 会覆盖原生语义并误导辅助技术。先修 HTML 结构和交互,再用 ARIA 补充原生语义无法表达的信息。
4. 表单和错误
interface EmailFieldProps {
error?: string;
}
export function EmailField({ error }: EmailFieldProps) {
return (
<div>
<label htmlFor="email">邮箱</label>
<input
id="email"
name="email"
type="email"
aria-invalid={Boolean(error)}
aria-describedby={error ? 'email-error' : 'email-help'}
/>
<p id="email-help">用于接收订单通知</p>
{error && <p id="email-error" role="alert">{error}</p>}
</div>
);
}
提交失败时:
- 页面级摘要说明有多少错误;
- 焦点移动到摘要或第一个错误字段;
- 字段有文本错误,不只使用红色边框;
- 保留用户已经填写的有效内容;
- 异步错误变化能够被辅助技术感知。
5. 拖拽和复杂交互
拖拽必须提供等价方式,例如:
- “上移”“下移”“移到顶部”按钮;
- 键盘抓取、移动、放下模式;
- 目标位置下拉选择;
- 操作后通过状态消息播报新位置。
Canvas、图表和可视化编辑器还应提供文本摘要、数据表或可导航的 DOM 控制层。
四、测试金字塔
| 层级 | 工具与方式 | 能发现什么 |
|---|---|---|
| 静态检查 | eslint-plugin-jsx-a11y | 常见语义错误 |
| 组件测试 | axe-core、Testing Library | 名称、角色、基础规则 |
| E2E | Playwright + axe | 页面集成、路由和状态 |
| 人工键盘 | 只用键盘完成任务 | 焦点、顺序、陷阱 |
| 辅助技术 | NVDA、VoiceOver 等 | 实际播报和操作体验 |
| 用户验证 | 残障用户可用性测试 | 自动化无法覆盖的真实障碍 |
自动化工具只能发现一部分问题,无法判断文字是否易懂、焦点顺序是否合理或复杂任务是否真正可完成。
五、组件库和 CI 治理
- 在 Button、Dialog、Form、Menu、Tabs 等基础组件中一次修复;
- Storybook 为各状态增加键盘和无障碍用例;
- 新增严重违规阻断 CI,存量问题建立 Baseline 并逐步清零;
- 设计稿评审检查对比度、焦点、错误和非颜色表达;
- 发布清单包含键盘冒烟和关键任务读屏回归;
- 无障碍问题有明确负责人和 SLA。
不要用一个全局规则忽略列表永久豁免。每个例外应有原因、负责人和到期时间。
六、衡量整改效果
推荐指标:
- 核心任务的键盘和读屏器成功率;
- P0/P1 未解决数量和平均修复时间;
- 新增无障碍回归数量;
- 组件库无障碍覆盖率;
- 自动化规则通过率,但不作为唯一结果;
- 客户投诉、支持工单和用户研究反馈。
常见面试问题
Q1: Lighthouse 100 分是否代表网站无障碍?
答案:不代表。自动化只能检查可机器判断的规则,无法完整验证焦点顺序、文本理解、读屏体验和任务是否能完成,仍需人工键盘与辅助技术测试。
Q2: tabindex 应该怎么用?
答案:原生可交互元素通常不需要。tabindex="0" 可把自定义元素放入自然顺序,-1 允许程序聚焦但不进入 Tab 顺序。应避免正数,因为会制造难维护的人工焦点顺序。
Q3: 弹窗关闭后焦点应该去哪?
答案:通常返回打开弹窗的触发元素;如果触发元素已经删除,则移动到逻辑上最接近、仍存在的控制点,并确保用户知道上下文变化。
Q4: 无障碍修复为什么应该进入组件库?
答案:弹窗、表单和菜单等问题会在大量页面重复。基础组件统一修复可以降低回归概率,并通过版本、测试和文档持续治理。
Q5: ARIA 能否替代语义化 HTML?
答案:不能。ARIA 主要补充语义,不自动提供键盘行为、焦点管理和浏览器原生能力。优先使用正确的原生元素。
Q6: 如何处理存量几百个无障碍问题?
答案:先按核心任务和严重程度分级,修复高复用基础组件,再处理页面特例;对新增问题设置门禁,对存量建立可下降的 Baseline,避免一次性大重构。