政务系统的前端无障碍改造复盘:WCAG 2.1 AA 级合规的落地路径

政务系统的前端无障碍改造复盘:WCAG 2.1 AA 级合规的落地路径 政务系统的前端无障碍改造复盘WCAG 2.1 AA 级合规的落地路径政务系统面向全体公民提供服务信息无障碍不是可选项而是合规刚需。本文复盘对某省级政务服务门户进行 WCAG 2.1 AA 级改造的完整过程从现状评估到最终验收梳理可复用的落地方法论。一、改造前的现状评估与问题分类改造前通过 axe-core 自动化扫描和人工走查相结合的方式对系统进行了全面评估。评估覆盖了首页、办事指南、在线申报、结果查询等 23 个核心页面累计发现 347 个无障碍问题。问题集中在六个方面颜色对比度不足大量浅灰色文字#999在白色背景上对比度仅 2.8:1远低于 WCAG AA 要求的 4.5:1缺少替代文本图标按钮、装饰图片、验证码图片均未提供文本替代键盘操作不可达下拉菜单、日期选择器、弹窗关闭按钮无法通过键盘触发表单标签缺失搜索框、筛选条件输入框未关联 label 元素焦点管理混乱弹窗打开后焦点未移入关闭后焦点未回到触发元素语义化结构缺失页面未使用地标角色banner、main、navigation屏幕阅读器用户难以快速定位区域问题的根源在于系统开发时未将无障碍纳入需求规格前端组件库未考虑 ARIA 支持UI 设计规范缺少颜色对比度和字体大小的约束。二、分阶段改造策略改造采用先基础后增强、先全局后局部的策略分为三个阶段推进。第一阶段基础合规是工作量最大但技术难度最低的阶段目标是消除所有自动化扫描可检测的问题。改造颜色系统是优先级最高的工作——修正了全局的 Design Token禁止使用对比度不达标的颜色组合。// design-tokens.ts — 无障碍合规的颜色系统 // 用途统一管理颜色变量确保所有色值组合满足 WCAG 2.1 AA 对比度要求 interface ColorToken { value: string; contrastOnWhite: number; // 白色背景上的对比度 contrastOnPrimary: number; // 主色背景上的对比度 } // 所有文本颜色值均在白色背景上满足 4.5:1 的最低对比度要求 export const textColors: Recordstring, ColorToken { primary: { value: #1a1a2e, // 对比度 17.2:1 ✓ contrastOnWhite: 17.2, contrastOnPrimary: 12.1, }, secondary: { value: #555770, // 对比度 6.2:1 ✓ contrastOnWhite: 6.2, contrastOnPrimary: 4.6, }, // 辅助文本色需满足大字体 3:1 标准此处使用 14px满足 4.5:1 tertiary: { value: #71718a, // 对比度 4.6:1 ✓ contrastOnWhite: 4.6, contrastOnPrimary: 3.1, // 不满足主色背景 — 禁止在有色按钮上使用 }, }; /** * 校验两个颜色值的对比度是否达标 * returns 对比度数值以及是否满足 AA 级要求 */ export function checkContrast( foreground: string, background: string, fontSize: number 14 ): { ratio: number; passesAA: boolean } { const ratio calculateWCAGContrast(foreground, background); // WCAG 2.1 AA 级小字 4.5:1大字18px 或 14px 加粗3:1 const threshold fontSize 18 || (fontSize 14 true) ? 4.5 : 3.0; return { ratio: Math.round(ratio * 10) / 10, passesAA: ratio threshold }; } /** 根据 sRGB 通道值计算相对亮度再导出对比度 */ function calculateWCAGContrast(hex1: string, hex2: string): number { const lum1 relativeLuminance(hex1); const lum2 relativeLuminance(hex2); const lighter Math.max(lum1, lum2); const darker Math.min(lum1, lum2); return (lighter 0.05) / (darker 0.05); } function relativeLuminance(hex: string): number { const r parseInt(hex.slice(1, 3), 16) / 255; const g parseInt(hex.slice(3, 5), 16) / 255; const b parseInt(hex.slice(5, 7), 16) / 255; const linearize (c: number) (c 0.04045 ? c / 12.92 : ((c 0.055) / 1.055) ** 2.4); return 0.2126 * linearize(r) 0.7152 * linearize(g) 0.0722 * linearize(b); }三、组件级的无障碍适配方案第二阶段聚焦交互组件重点解决键盘操作和焦点管理问题。以政务系统中最高频的日期选择器为例// AccessibleDatePicker.tsx — 无障碍日期选择器 // 用途替代原生 input[typedate] 的自定义日期组件满足键盘操作和屏幕阅读器需求 import { useState, useRef, useEffect, useCallback } from react; interface DatePickerProps { value: string; onChange: (date: string) void; label: string; placeholder?: string; } export const AccessibleDatePicker: React.FCDatePickerProps ({ value, onChange, label, placeholder 请选择日期, }) { const [isOpen, setIsOpen] useState(false); // 焦点管理记住触发元素关闭弹窗时焦点回归 const triggerRef useRefHTMLButtonElement(null); const panelRef useRefHTMLDivElement(null); const idPrefix useRef(datepicker-${Math.random().toString(36).slice(2, 9)}); /** 打开日期面板焦点移到第一个日期按钮 */ const openPanel useCallback(() { setIsOpen(true); // 延迟聚焦等面板 DOM 渲染完毕 requestAnimationFrame(() { const firstDateBtn panelRef.current?.querySelectorHTMLElement( [rolegridcell]:not([aria-disabledtrue]) ); firstDateBtn?.focus(); }); }, []); /** 关闭面板焦点回到触发按钮 */ const closePanel useCallback(() { setIsOpen(false); triggerRef.current?.focus(); }, []); // ESC 键关闭面板 const handleKeyDown useCallback( (e: React.KeyboardEvent) { if (e.key Escape) { e.stopPropagation(); closePanel(); } // 日期网格内的方向键导航由 onGridKeyDown 处理 }, [closePanel] ); // 点击外部关闭useEffect 绑定全局事件 useEffect(() { if (!isOpen) return; const handleClickOutside (e: MouseEvent) { const target e.target as HTMLElement; if (panelRef.current !panelRef.current.contains(target)) { closePanel(); } }; document.addEventListener(mousedown, handleClickOutside); return () document.removeEventListener(mousedown, handleClickOutside); }, [isOpen, closePanel]); return ( div classNamedatepicker-wrapper onKeyDown{handleKeyDown} {/* 标签显式关联htmlFor id屏幕阅读器可识别 */} label htmlFor{${idPrefix.current}-trigger} classNamedatepicker-label {label} /label {/* 使用 button 而非 div天然支持键盘聚焦 */} button ref{triggerRef} id{${idPrefix.current}-trigger} classNamedatepicker-trigger onClick{() (isOpen ? closePanel() : openPanel())} aria-expanded{isOpen} aria-haspopupdialog aria-label{${label}当前值为 ${value || 未选择}} {value || placeholder} span aria-hiddentrue classNamecalendar-icon/span /button {/* 日期选择面板roledialog aria-modaltrue */} {isOpen ( div ref{panelRef} roledialog aria-modaltrue aria-label{选择${label}} classNamedatepicker-panel {/* 日历网格省略具体实现核心无障碍属性如下 */} {/* rolegrid aria-label日期选择日历 */} {/* 每个日期rolegridcell已选中的添加 aria-selectedtrue */} {/* 不可选日期aria-disabledtrue */} {/* 方向键在日期之间移动聚焦 */} /div )} /div ); };四、CI/CD 集成与持续性保障无障碍改造最容易被忽视的环节是回归问题。改造完成后一个月内就出现了 12 处由新功能引入的无障碍回退。解决方法是将无障碍检查集成到 CI/CD 流水线作为与单元测试同等级的阻断条件。// a11y-ci.config.ts — CI 无障碍检查配置 // 用途在 CI 流水线中自动运行 axe-core 扫描违规数超过阈值则阻断构建 export default { // 扫描页面列表生产环境路由 urls: [ /home, /service-guide, /online-apply, /result-query, /personal-center, ], // 每个页面的违规上限 errorThreshold: 0, // error 级别违规零容忍 warningThreshold: 3, // warning 级别最多 3 个 // 排除规则已知的第三方组件问题已有修复计划 excludeRules: [ color-contrast, // 第三方图表库的对比度问题已反馈给厂商 ], // 支持 WCAG 2.1 AA 标签集 runOnly: [wcag2a, wcag2aa, wcag21a, wcag21aa], // 超时设置政务系统部分页面涉及复杂表单渲染较慢 timeout: 30000, };改造验收阶段的数据自动化扫描发现问题数从 347 降至 0人工走查发现问题数从 83 降至 7。剩余的 7 个问题全部集中在动态内容播报aria-live 区域更新时机因为涉及复杂的异步交互逻辑已列入下一期优化计划。五、总结政务系统 WCAG 2.1 AA 改造的核心经验第一不要试图一次性修完所有问题。按自动化可检测 → 交互可达 → 动态可感知分阶段推进每个阶段的成果可验证、可度量。第二Design Token 层面的颜色改造收益最大。一次修改覆盖全局从源头消除对比度问题后续的新增页面天然合规。第三CI/CD 集成是防止回退的唯一可靠手段。无障碍不是一次性改造而是在每次代码变更中都保持合规的工程实践。改造后的系统通过了省级无障碍专项评测屏幕阅读器用户的任务完成率从 47% 提升至 89%。对于其他政务系统或 2G 产品的无障碍改造这套方法论可直接复用。