前端无障碍实战指南:从颜色对比度到屏幕阅读器的完整适配
前端无障碍实战指南从颜色对比度到屏幕阅读器的完整适配一、无障碍不是政治正确是设计质量的基础指标做无障碍Accessibility简称 A11y这件事大多数前端开发者的第一反应是我们产品还在抢用户哪有时间做这个但如果你换个角度看——无障碍适配的每一项要求本质上都是在提升产品的设计质量颜色对比度 ≥ 4.5:1 → 户外强光下的可视性更好键盘可操作 → 效率型用户的工作流更流畅语义化 HTML → SEO 更好、代码更易维护焦点管理 → 模态框/弹窗的交互更清晰这篇文章我不会跟你讲无障碍很重要你肯定听够了我会给你一套可以立刻落地到项目里的具体方法——从最基础的颜色对比度检查到屏幕阅读器的完整适配每一步都带具体代码。二、无障碍适配的四层模型Layer 1视觉层颜色对比度检查的精确方法不要靠肉眼判断。WCAG 2.1 AA 标准要求普通文字18px对比度 ≥ 4.5:1大文字≥18px 或 ≥14px 加粗对比度 ≥ 3:1UI 组件和图形对象对比度 ≥ 3:1/** * WCAG 对比度计算器 * * 公式contrast (L1 0.05) / (L2 0.05) * 其中 L 是相对亮度Relative Luminance */ function getContrastRatio(hex1: string, hex2: string): number { const l1 getRelativeLuminance(hex1); const l2 getRelativeLuminance(hex2); const lighter Math.max(l1, l2); const darker Math.min(l1, l2); return (lighter 0.05) / (darker 0.05); } function getRelativeLuminance(hex: string): number { const { r, g, b } hexToRgb(hex); // sRGB 到线性 RGB 的转换 const linearize (c: number): number { c / 255; return c 0.03928 ? c / 12.92 : Math.pow((c 0.055) / 1.055, 2.4); }; // 加权求和基于人眼对不同颜色的敏感度 return ( 0.2126 * linearize(r) 0.7152 * linearize(g) 0.0722 * linearize(b) ); } function hexToRgb(hex: string) { hex hex.replace(#, ); return { r: parseInt(hex.substring(0, 2), 16), g: parseInt(hex.substring(2, 4), 16), b: parseInt(hex.substring(4, 6), 16), }; } // 使用示例 const ratio getContrastRatio(#64748B, #FFFFFF); console.log(对比度: ${ratio.toFixed(2)}:1); // → 对比度: 4.65:1 ✓ 通过 AA 标准焦点指示器的正确做法浏览器默认的焦点轮廓outline在 Chrome 下是蓝色虚线框很多设计师觉得丑就用outline: none把它干掉了。这是无障碍的红色警报。正确的做法是自定义焦点样式而不是移除它/* 使用 :focus-visible只在键盘导航时显示焦点 */ :focus { outline: none; } :focus-visible { outline: 3px solid #3B82F6; outline-offset: 2px; border-radius: 2px; } /* 暗黑模式下的焦点样式 */ media (prefers-color-scheme: dark) { :focus-visible { outline-color: #60A5FA; outline-offset: 2px; } }Layer 2结构层语义化 HTML 是无障碍的基础。以下是最重要的几个 Landmark 角色HTML 元素隐式 ARIA Role用途headerbanner页面头部navnavigation导航区域mainmain每页只有一个主要内容sectionregion需 aria-label内容区块articlearticle独立内容单元asidecomplementary侧边栏footercontentinfo页面底部标题层级Heading Hierarchy检查!-- 错误跳级 -- h1页面标题/h1 h3二级标题/h3 !-- 不应该从 h1 直接跳到 h3 -- !-- 正确严格逐级 -- h1页面标题/h1 h2章节标题/h2 h3小节标题/h3 h3另一个小节/h3 h2另一个章节/h2Layer 3交互层模态框Modal的焦点陷阱这是无障碍实践中出错最多的地方。当模态框打开时焦点必须困在模态框内——用户按 Tab 键不应该跑到模态框背后的页面元素上。/** * 焦点陷阱 HookReact 示例 */ function useFocusTrap(containerRef: React.RefObjectHTMLElement, isActive: boolean) { useEffect(() { if (!isActive || !containerRef.current) return; const container containerRef.current; const focusableSelector [ a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex-1]), ].join(, ); function handleKeyDown(e: KeyboardEvent) { if (e.key ! Tab) return; const focusable container.querySelectorAll(focusableSelector); const first focusable[0] as HTMLElement; const last focusable[focusable.length - 1] as HTMLElement; if (e.shiftKey) { // ShiftTab: 从第一个跳到最后一个 if (document.activeElement first) { e.preventDefault(); last?.focus(); } } else { // Tab: 从最后一个跳到第一个 if (document.activeElement last) { e.preventDefault(); first?.focus(); } } } // 打开时聚焦到第一个可聚焦元素 const firstFocusable container.querySelector(focusableSelector) as HTMLElement; firstFocusable?.focus(); container.addEventListener(keydown, handleKeyDown); return () container.removeEventListener(keydown, handleKeyDown); }, [isActive]); }Layer 4内容层图片 Alt 文本的写作规则!-- 装饰性图片空 alt -- img srcdecorative-line.svg alt / !-- 功能性图片描述功能不是外观 -- img srcsearch-icon.svg alt搜索 / !-- 信息性图片描述关键信息 -- img srcchart.png alt2024年Q1-Q4营收趋势图从120万增长到380万 / !-- 复杂图片简短 alt 详细描述 -- img srcarchitecture.png alt系统架构图 aria-describedbyarch-detail / div idarch-detail classsr-only 系统由4层组成接入层(Nginx负载均衡)、应用层(Spring Boot微服务)、 数据层(MySQL主从Redis集群)、监控层(PrometheusGrafana)。 /div屏幕阅读器专用样式.sr-only.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } /* 注意不要用 display:none 或 visibility:hidden 这会让屏幕阅读器也读不到内容 */四、快速检查清单与自动化工具每日自检清单5 分钟Tab 键走一遍从地址栏开始按 Tab能到达所有交互元素吗焦点顺序合理吗颜色对比度用 Chrome DevTools 的 CSS Overview 面板一键检查CtrlShiftP → Show CSS Overview。标题层级用浏览器插件 HeadingsMap 查看页面标题结构是否逐级。图片 Alt用 WAVE 插件检查是否有图片缺少 Alt 属性。缩放测试将浏览器缩放到 200%页面内容是否仍然完整可读自动化检测接入 CI// package.json { scripts: { lint:a11y: axe-linter src/, test:a11y: cypress run --spec cypress/e2e/a11y.cy.ts } }使用axe-core/react在开发环境实时检测cypress-axe在 CI 中跑关键页面的无障碍测试。五、总结无障碍不是额外的功能需求而是基础的设计质量指标。它的每一项约束——颜色对比度、键盘可操作、语义化结构、内容可读——都不是专门为残障用户设计的而是让所有人在所有场景下都能更好地使用你的产品。从今天开始少用一个div多用一个button少写一个outline:none多写一个自定义焦点样式。这些微小的改变积累起来就是产品的质感飞跃。作者李慕杰Leo / 8limujie一个终于不再用 div 包一切的正在赎罪的前端匠人