跳到主要内容

线上无障碍问题如何整改

场景

产品收到客户投诉或合规审计报告:键盘无法完成核心流程、弹窗焦点丢失、表单错误读屏器不播报、拖拽功能没有替代方式。作为前端负责人,如何止血、整改并建立长期防线?

面试速答版

无障碍整改不能只跑一次 Lighthouse:

  1. 确认影响用户和完整任务路径,登录、支付、提交等阻断问题按线上故障处理;
  2. 用 WCAG 成功标准给问题编号和定级,保留浏览器、辅助技术和复现步骤;
  3. 先修语义和原生交互,再补 ARIA;优先键盘、焦点、名称/角色/状态和表单错误;
  4. 自动化扫描发现基础问题,人工键盘测试和真实读屏器验证行为;
  5. 为拖拽、悬停、颜色提示和超时流程提供等价替代;
  6. 修复进入组件库、Storybook、测试和 CI 门禁,避免页面逐个打补丁;
  7. 用任务成功率、阻断问题数和回归率衡量,而不只看自动化分数。

一、先做影响分级

等级判断标准示例处理
P0核心任务完全无法完成且无替代键盘无法登录或付款立即止血和热修
P1关键功能严重受阻弹窗焦点陷阱、错误不播报当前迭代修复
P2体验明显下降但可绕过焦点样式不清晰、标题层级混乱排期治理
P3最佳实践和一致性问题冗余播报、描述不够简洁组件升级处理

严重程度应结合用户影响和任务链路,不应仅按 WCAG A、AA、AAA 机械映射。

二、建立可复现的审计记录

每个问题至少记录:

  • URL、组件、操作步骤和期望结果;
  • 浏览器、操作系统、缩放比例;
  • 辅助技术及版本,例如 NVDA、VoiceOver;
  • 对应 WCAG 成功标准;
  • 截图、录屏或可访问性树证据;
  • 负责人、修复版本和回归用例。
a11y-issue.ts
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. 优先使用原生语义

原生 buttoninputlabeldialog 和标题结构通常已经提供键盘行为与可访问性语义。把 div 加上 role="button" 后,还需要自己实现焦点、Enter/Space、禁用状态等行为,维护成本更高。

2. 键盘和焦点

检查:

  • 所有交互是否能用 Tab、Shift+Tab、Enter、Space、方向键完成;
  • 焦点顺序是否符合视觉和任务顺序;
  • 焦点样式是否清晰且未被 Sticky 区域遮挡;
  • 弹窗打开后焦点是否进入,关闭后是否回到触发元素;
  • 是否出现键盘陷阱或不可达控件。
accessible-dialog.tsx
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 状态必须与真实视觉和交互状态同步。
No ARIA is better than bad ARIA

错误的 ARIA 会覆盖原生语义并误导辅助技术。先修 HTML 结构和交互,再用 ARIA 补充原生语义无法表达的信息。

4. 表单和错误

accessible-field.tsx
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名称、角色、基础规则
E2EPlaywright + 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,避免一次性大重构。

相关链接