数字产品中的残疾偏见很少以直接的排斥出现,更多时候它固化在一行没有焦点轮廓的 CSS 里、一个只用颜色区分状态的表单提示中,或者一段无法被屏幕阅读器识别的图标按钮上。消除这种偏见,需要把无障碍从测试阶段的修复项,前移到设计系统和组件实现阶段。技术表现、组件重构、测试审查和团队流程,是四个不可绕开的层面。

一、残疾偏见在产品中的四种技术表现
第一种常见表现是语义缺失导致的键盘不可达。很多自定义组件用 <div> 或 <span> 模拟按钮、菜单和选项卡,只绑定鼠标事件。屏幕阅读器无法把这些元素识别为可交互控件,键盘用户按 Tab 键时会直接跳过,更无法通过 Enter 或空格触发操作。一个典型的错误做法如下:
<!-- 错误示例:div 只有鼠标事件,键盘用户无法触发 --> <div class="btn" onclick="submitForm()">提交</div> <!-- 正确示例:原生 button 提供键盘与语义支持 --> <button type="button" class="btn" onclick="submitForm()">提交</button>
正确做法是优先使用原生 <button> 元素,它天然具备焦点、键盘事件和可访问名称。如果业务确实要求非标准控件,则需要补充 role、tabindex 以及键盘事件处理。但多数场景下,原生元素远比 ARIA 模拟更可靠。
第二种表现是视觉对比度不足和颜色依赖。WCAG 2.2 的 SC 1.4.3 要求普通文本对比度至少达到 4.5:1,大号文本至少 3:1。但不少界面为了追求柔和视觉,把辅助文字、占位符和禁用状态做成浅灰色,实际对比度常常低于 3:1。更隐蔽的问题是只用红色和绿色表示成功或失败,色觉障碍用户无法区分状态变化。正确的方式是在颜色之外增加图标、文字标签或纹理。表单错误提示不能只让边框变红,还应显示明确的文字说明。
第三种表现是动态内容和表单反馈缺乏暴露。轮播图自动播放却没有暂停按钮,倒计时结束不通知辅助技术,表单提交失败后错误信息只出现在页面顶部,焦点仍然停留在提交按钮上。这些都会让使用屏幕阅读器的用户错过关键状态。可以通过 aria-live 区域、role="alert" 以及焦点管理,让状态变化主动被读屏软件捕获。
二、用包容性设计原则重构关键前端组件
组件层面的无障碍改造,不能只靠开发阶段补丁,而应进入设计系统的默认规范。焦点状态是最直观的指标:很多项目为了美观直接使用 outline: none,但这会让键盘用户完全失去当前位置。可访问的焦点样式应该使用高对比度轮廓、边框或阴影,并在所有交互元素上保持一致。触摸目标尺寸方面,移动端至少需要 24x24 CSS 像素,推荐达到 44x44 像素,以覆盖手指和运动控制受限的用户。
模态对话框是最容易产生焦点陷阱的组件之一。打开对话框后,焦点应移动到对话框内部第一个可交互元素;Tab 键应在对话框内循环,不能跑到背景内容;Esc 键应能关闭对话框,关闭后焦点要回到触发按钮。下面是一个不依赖第三方库的基础实现:
let lastFocusedElement = null;
let activeDialog = null;
function openDialog(dialog) {
activeDialog = dialog;
lastFocusedElement = document.activeElement;
dialog.showModal();
const focusable = getFocusableElements(dialog);
if (focusable.length > 0) {
focusable[0].focus();
}
dialog.addEventListener('keydown', handleDialogKeydown);
}
function getFocusableElements(container) {
return container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
}
function handleDialogKeydown(event) {
if (event.key === 'Escape') {
activeDialog.close();
lastFocusedElement.focus();
activeDialog.removeEventListener('keydown', handleDialogKeydown);
activeDialog = null;
return;
}
if (event.key === 'Tab') {
const focusable = getFocusableElements(activeDialog);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
last.focus();
event.preventDefault();
} else if (!event.shiftKey && document.activeElement === last) {
first.focus();
event.preventDefault();
}
}
}
原生 <dialog> 元素已经提供 showModal() 方法,可以简化部分工作,但仍需处理焦点循环和返回焦点。如果项目需要兼容较老浏览器,建议使用经过测试的无障碍组件库,而不是自己从头实现。
表单无障碍同样需要从结构上修正。每个输入框都应通过 <label> 的 for 属性与控件关联,而不是只依赖占位符。单选按钮和复选框使用 <fieldset> 与 <legend> 分组。错误提示可以通过 aria-describedby 关联到具体控件,并用 aria-invalid="true" 标记出错状态。提交失败后,焦点应自动移动到错误摘要区域,便于读屏用户立即感知。
三、自动化测试与人工审查:把问题拦截在合入之前
自动化工具是发现无障碍问题的最低成本入口。axe-core 能检测出大约 30% 到 50% 的常见问题,比如图片缺失 alt 文本、按钮缺少可访问名称、颜色对比度不足、ARIA 使用错误等。将它集成到端到端测试中,可以在每次提交前自动扫描关键页面。Playwright 与 axe-core 的配合比较简单:
const { test, expect } = require('@playwright/test');
const { AxeBuilder } = require('@axe-core/playwright');
test('首页无障碍扫描', async ({ page }) => {
await page.goto('https://ipipp.com/');
const results = await new AxeBuilder({ page }).analyze();
const serious = results.violations.filter(v => v.impact === 'serious' || v.impact === 'critical');
expect(serious).toEqual([]);
});
不过自动化无法覆盖所有体验问题。Tab 顺序是否合理、焦点是否始终可见、屏幕阅读器朗读是否自然、操作节奏是否过于急促,这些都需要人工走查。键盘走查可以用最朴素的方法:关闭鼠标,只使用 Tab、Shift+Tab、Enter、空格和方向键完成核心流程。屏幕阅读器测试则至少覆盖 VoiceOver 和 NVDA 一种组合,检查标题层级、地标区域和表单标签是否读得清楚。
持续集成中的无障碍断言,不建议一开始就把历史问题全部设为失败,否则团队容易直接关闭检查。更务实的做法是设定阈值,先阻断 critical 和 serious 级别问题,同时把 moderate 和 minor 问题输出为报告,安排迭代修复。Lighthouse CI 或 pa11y 都能生成可追踪的结果,帮助团队看到趋势。
四、从团队流程中根除偏见:合规之外的包容策略
技术修复只能解决已经发生的问题,要根除残疾偏见,必须把包容性前置到需求与设计阶段。产品经理在写用户故事时,就应包含键盘操作、屏幕阅读器朗读、对比度和焦点管理等验收条件。例如登录表单的用户故事,除了输入账号密码并跳转之外,还应明确“页面加载后焦点自动进入第一个输入框,提交失败时错误信息能被读屏软件朗读”。这样开发者在实现时就不会把无障碍当作额外事项。
真实用户测试是发现认知偏见和操作障碍的关键。自动化和专家走查无法模拟所有残障场景,比如注意力障碍用户在过多动效下的体验、肢体障碍用户对目标精度的要求、认知障碍用户对文案复杂度的理解。与残障社群和当地无障碍组织合作,招募多样化测试者,可以获得工具覆盖不到的反馈。测试目标不应只是合规,而是确认用户能否独立、高效地完成任务。
文档和组件清单是把无障碍沉淀为工程能力的最后一步。建设一个“无障碍组件库”,记录每个组件的键盘行为、ARIA 模式、触摸尺寸和已知限制。定义缺陷等级,例如缺失替代文本、焦点不可见属于 P0,必须在上线前修复;装饰性动效过多可能属于 P1,需要降低频率或提供关闭入口。通过定期的无障碍培训和设计评审,让产品、设计、开发和测试形成共同语言,残疾偏见才不会在一轮轮迭代中反复出现。

