
我做前端这些年真正把前端可访问性Accessibility当回事是源于一次用户反馈。有个“校友资料查询”的功能上线产品经理转述一位用户的疑问为什么列表里的姓名用 Tab 键选不中。我们当时查了半天代码按钮、链接都是正常写的直到我用键盘走查才发现那条列表项的“查看详情”入口是一个自定义 div 加 click 事件鼠标能点键盘却始终无法聚焦读屏用户自然也无从入手。这个功能本质上就是对于某些人它的门是关着的。这让我意识到一个问题前端可访问性从来不是一个“锦上添花”的加分项它决定了一个功能到底面向哪些人又把哪些人挡在门外。这篇内容想从一个前端开发者的视角把可访问性这件事拆开讲清楚——它服务的对象到底是谁、最常见的坑出现在哪里、应该如何修以及如何把检查融入日常开发流程而不是每次上线前才临时突击。适合前端开发者、组件库维护者也适合那些隐隐觉得产品“对某些用户不太友好”但不知道从哪抓起的同学。1. 重新认识被关在门外的人可访问性不仅是“给盲人做优化”1.1 一张用户谱系图永久、临时、情境性障碍很多人一提到可访问性第一反应是“给盲人读屏用的”。这个刻板印象没错但把范围窄化了。无障碍领域常用一个用户谱系来展示受益人群永久性障碍比如失明、失聪、行动不便临时性障碍比如手臂骨折、眼睛手术后暂时模糊情境性障碍比如户外强光下看不清屏幕、开会时不能开声音、双手抱娃只能用单手操作、甚至只是今天忘带耳机但在地铁上想看视频。对一个互联网产品来说今天访问你网站的大部分用户可能没有任何障碍但几乎每个用户一生中都会经历至少一次临时性障碍或情境性障碍。从这个角度看可访问性提升的不只是残障群体的体验而是所有人的体验上限。比如表单里的 label 与输入框正确关联读屏用户能读出“用户名”普通用户点击文字也能聚焦输入框视频字幕最初的服务对象是听障用户但最后受益的是在通勤地铁上不带耳机的所有人。这个思维转换很重要它决定了你做可访问性是“应付标准”还是“顺势把产品做好”。1.2 商业、法律与职业层面的现实驱动力除了人文视角商业层面的理由也很具体。全球有相当比例的成年人存在某种程度的视觉障碍或其他障碍他们同样有消费力、同样会被产品体验影响决策。对面向公众的产品来说可访问性改进直接带来的是客服投诉减少、用户转化路径更顺畅而语义化结构对搜索引擎也更友好机器友好的内容通常对人更友好。对前端开发者个人来说可访问性技能在面试和团队评审中的分量越来越重。这几年我面试前端岗位必问的一道题是“如果用户用键盘而不是鼠标操作你的页面有哪些功能会不可用”能深入回答的人往往对交互细节有极强的掌控力写出来的代码也更有结构性。这个技能不会过时因为用户群体的多样性和法律法规的强制性都在推着行业往前走提前掌握的人只会越来越占优势。2. 键盘可达性是最低门槛从一次 Tab 走查排障讲起2.1 为什么先看键盘因为鼠标不是唯一的指针WCAGWeb Content Accessibility Guidelines把“可操作”作为四大原则之一而键盘可达性是最直接的操作性指标。原因非常简单屏幕阅读器用户、部分运动障碍用户、重度快捷键用户都不依赖鼠标。如果一个交互无法用键盘完成对这些人来说它就是完全不存在的。开发中常见的键盘不可达问题主要有三类自定义元素没有 tabindex焦点根本没有进入可聚焦序列有 tabindex但操作事件只绑定了 click没有监听键盘事件焦点进去了出不来也就是“焦点陷阱”最常见于弹窗、日期选择器这类浮层组件。这三种问题我几乎每周都能在别人的代码里看到自己早期也踩过不止一次。除非团队里有人专门负责可访问性审查否则这类问题很容易在自测时被忽略——因为绝大多数开发者的日常操作都是鼠标优先。2.2 一次实际的走查排障过程具体排查方法并不复杂养成习惯是关键就是一个一个按 Tab。以我经历过的“查询结果列表不可键盘访问”为例完整链路是打开页面确认焦点起始位置连续按 Tab观察焦点是否按预期顺序在搜索框、按钮、列表、翻页控件之间移动走到某个“查看详情”入口时焦点直接跳过因为它是一个 div没有 tabindex这时不要急着加 tabindex先找产品确认这个入口的语义。它的行为是触发一个查看动作本质就是一个按钮改法是把 div 换成 button 标签而不是硬加 tabindex换完后 Tab 能聚焦了但按回车没反应因为 click 事件原来写在 div 的 onClick 上换成 button 后验证原生 Enter 是否能触发 click确认触发逻辑无误再检查焦点样式是否可见最后回归一遍主流程。这里要特别注意两点。第一不要给所有 div 都塞 tabindex0 让它们变成可聚焦。tabindex 的引入会改变 Tab 的天然顺序使用混乱会让读屏用户和键盘用户更困惑。第二原生 button 天然支持 Enter 和 Space 触发 click这是自定义 div 永远补不全的能力。能用原生标签就用原生标签这个原则在可访问性里非常重要。2.3 可见焦点Tab 到了但你看不见键盘可达的另一个隐藏坑是焦点样式被干掉。很多团队为了视觉上的简洁在 CSS 里写button:focus { outline: none; }导致用户 Tab 过来时界面上没有任何视觉反馈。对低视力用户和键盘用户来说这无异于在黑暗里开车却被拿掉了车灯。我的习惯是保留并自定义焦点样式不直接用 outline: none。可以用 outline 或更丰富的 box-shadow 方案但核心是保证焦点位置清晰、对比度明显。WCAG 2.4.7 要求可见焦点这属于 AA 级别。如果团队里有 UI 设计师建议把 focus 样式纳入设计规范和 hover 一样对待而不是开发时顺手抹掉。一个容易被接受的说法是焦点样式不是丑陋的虚线它是交互的“当前位置指示器”没有它键盘用户就迷失了。3. 语义化 HTML 不是玄学它是辅助技术读懂页面的地图3.1 读屏器到底在做什么读屏器屏幕阅读器并不会像人一样“看到”整个页面布局。它在浏览网页时依赖的是文档的语义结构标题标签构建导航菜单列表标签告诉它这里有几项按钮和链接各自拥有预设的角色和快捷键。如果你的页面全是 div 和 span读屏用户得到的就是一串没有结构的文本体验就像听人用没有任何停顿的语气念一整本没有目录的书。这就是为什么语义化 HTML 是前端可访问性的地基。header、nav、main、footer、aside 这些 landmark 标签让读屏用户可以在不同区块之间快速跳转h1-h6 建立文档大纲读屏用户可以按标题索引导航button 和 a 天然具备可交互角色和键盘事件支持。如果这些不做后面加再多 ARIA 都只是在打补丁补丁打多了页面性能和维护成本都会失控。3.2 表单里 label 的正确关联表单是另一个高发区。最典型的错误是用 placeholder 代替 label。很多产品为了界面简洁只放 placeholder用户一输入提示文字就消失对认知负担较重的用户和有记忆障碍的用户非常不友好。而且部分读屏对 placeholder 的支持并不可靠可能根本读不出提示内容。正确做法是给每个输入项配 label用 for 关联 input 的 id或者直接用 label 包裹 input。需要注意可见的 label 总是比隐藏的 label 更好。如果设计上确实不能显示可见 label那也要用 aria-label 或 aria-labelledby 提供可访问名称而不是什么都不给。比如一个搜索框旁边放放大镜图标按钮这个按钮的可访问名称应该是“搜索”而不是“放大镜”或“按钮”。3.3 图片与表格内容缺失时信息就断了图片 alt 属性很多人知道要加但经常写错。alt 的原则是描述图片的功能和传达的信息而不是描述图片本身。比如一张“本月销量上升趋势”的图表alt 应该写“本月销量从 X 上升到 Y增长 Z%”而不是“图表”。如果图片本身只是个装饰分割线直接用 alt 让读屏跳过反而更干净。表格的无障碍常用 th 加 scope 来实现。给表头单元格设置th scopecol或scoperow读屏用户按行列浏览时能知道当前单元格对应的字段是什么。很多人习惯用 td 然后把样式加粗这在视觉上没问题但对读屏用户来说表头和数据之间的关联就断了。表格如果很复杂还可以用 caption 给表格一个标题让读屏用户先知道这张表讲的是什么。4. ARIA 的正确打开方式能不用就不用要用就用到点子上4.1 ARIA 不是万能膏药ARIAAccessible Rich Internet Applications是一套补充语义的属性体系但它并不能改变浏览器原生行为的细节。社区有句话叫“ARIA 的第一规则是不要用 ARIA除非原生语义真的无法覆盖”。滥用 ARIA 的典型场景包括给原生 button 加 rolebutton原生的本来就是 button加了等于加了一块遮羞布有时反而让屏幕阅读器重复播报给 div 加 rolebutton 却只处理 click键盘支持没有真正补全用 aria-label 覆盖可见文本但文案不一致读屏用户听到的和弱视用户看到的是两套信息会产生矛盾。正确的思路是优先原生 HTML原生覆盖不了的交互模式比如自定义弹窗、手风琴、菜单、滑块才需要 ARIA 做辅助。ARIA 能做的只是“补充说明”不是“重塑交互”。4.2 弹窗、菜单、手风琴的处理套路以弹窗为例一个相对完整的可访问性方案包括roledialog 加 aria-modaltrue 标明模态对话框aria-labelledby 指向弹窗标题 id读屏用户一进入就知道这个弹窗是干什么的打开弹窗后焦点移入弹窗焦点约束在弹窗内部Tab 和 ShiftTab 不能跑到页面背景里关闭后焦点回到打开弹窗的按钮上按 Esc 关闭弹窗并恢复焦点。这套规则里最容易漏掉的是“关闭后焦点回到入口”和“Esc 关闭”。前者对键盘走查用户来说特别明显——关完弹窗焦点还停在 body 上下一次 Tab 直接跳到页面开头跳跃感很强。菜单组件要注意展开收起时 aria-expanded 状态的同步切换手风琴内容在展开前不应该让读屏读到大段隐藏文本通常用 hidden 属性或对隐藏元素做处理。这些不是标准答案但它们是过去二十年社区踩坑沉淀出来的通用模式照着做不会错。4.3 aria-live让动态内容“说人话”页面里异步加载的内容、错误提示、表单校验信息如果不处理读屏用户完全不知道页面已经变了。这时候需要 aria-live 区域aria-livepolite 适合普通通知aria-liveassertive 适合紧急错误。但使用时要有节制实时滚动的时间更新、频繁的数值变化不要一直“说话”否则会干扰读屏用户获取信息。我最常用的是错误提示容器设置 rolealert它本质上是 assertive live 区域。提交表单校验失败时把错误信息塞进这个容器读屏用户就能立刻听到。这里有个坑如果容器本身是在页面加载后动态创建的某些读屏可能来不及建立链接。稳妥做法是页面初始渲染时就放一个空的 rolealert 容器后续只改它的文本内容这样大多数读屏都能稳定播报。这个细节属于“不踩一次根本不知道存在”的问题所以组件库代码里如果提前内置了业务方会很省心。5. 颜色、对比度和动效视觉层面的那些“看不见”的门槛5.1 对比度不是“能看清”就行WCAG 2.x 对文本对比度的要求是普通正文至少 4.5:1AA大字或加粗至少 3:1UI 组件如输入框边框、焦点指示也建议达到 3:1。我几乎每次设计评审都会拿 WebAIM 的 Contrast Checker 验证一遍颜色尤其喜欢用浅灰文字配白色背景的团队对比度常常只有 2:1 甚至更低。年轻的同事会说“我觉得能看清”但换个屏幕亮度、年龄大一点、在户外阳光下这个“能看清”就不成立了。对比度背后的计算是相对亮度公式不是肉眼猜。实际开发中不用手算直接用工具验证但要把这个指标写进设计规范和组件库里而不是上线前才补。比如按钮文字色、正文字色、占位提示文字色都要给出明确的色板对比度标注。如果设计工具支持对比度检测就更好可以直接在设计阶段卡住。5.2 不要只用颜色传达状态另一个高频问题是“只用颜色”区分信息。比如表单校验错误时输入框边框变红却没有文字提示状态标签只有绿色和红色的背景区分。对色觉障碍用户来说红色和绿色可能是无法区分的。修复办法很简单每个用颜色传达的信息至少有一个非颜色的冗余信息。比如加图标、加“错误密码不能少于 8 位”这样的文案或者给按钮加 aria-label。这不是多此一举而是信息冗余的基本功。页面上的“状态”信息宁可多说一句也不要让任何群体靠猜。5.3 为动效用户留一条退路prefers-reduced-motion动效是近几年的大坑。很多站点为了“高级感”加入大量入场动画、视差滚动、无限循环背景但这类动效对前庭功能障碍用户可能引发眩晕和恶心。CSS 媒体查询 prefers-reduced-motion 正好用于这个场景media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } }这段代码可以让主动声明“减少动态效果”的用户在系统中移除动画和过渡。它还应该配合 JavaScript 侧的功能判断比如轮播图自动播放、横幅滚动这些在用户启用减少动效时直接关闭。需要重点说明的是真正的可访问性不是靠 CSS hack 一刀切而是要求设计师提供静态替代方案。如果你只是把动画从 0.3 秒改成 0.01 秒内容本身仍然要能完整理解和操作。6. 内容级障碍验证码、时间限制与认知负担6.1 验证码把一部分人挡在登录门外验证码大概是可访问性领域最著名的“门禁”。图形验证码要求用户识别扭曲的字母视障用户基本无法完成。常见的替代方案包括逻辑验证码比如“3 加 4 等于几”无障碍验证码服务以及邮箱或短信验证码。还有一条原则验证码不应该成为完全无法绕过的唯一关卡应该提供可选的听觉方案或人工客服兜底。我的实际经验是如果产品对验证码的需求主要是防机器人优先考虑服务端频率限制和风险策略而不是把验证成本转嫁给所有真实用户。这句话值得对需求方反复强调。用图形验证码挡住的不只是脚本还有真实存在的人类用户。商业产品的第一原则是让真实用户顺畅通过而不是让机器的对手更难受。6.2 倒计时与自动跳转另一个常被忽略的门槛是时间限制。诸如“60 秒后自动跳转”“30 秒内未操作订单取消”这类设计对阅读速度慢的用户、使用读屏的用户都有不小压力——读屏用户浏览速度天然比视觉用户慢同样的内容他们需要更多时间处理。解决思路包括提供“延长会话”按钮在倒计时开始前明确告知并留出足够准备时间考虑取消自动跳转改为按钮触发。如果产品方坚持要自动跳转至少要保证剩余时间可见并且跳转前能取消。6.3 清晰文案也是一种可访问性最后是认知层面的简化。认知障碍用户对长难句、双关语、突然出现的专业术语有理解困难。可访问性领域有一条原则不要使用依赖特定文化背景才能理解的表达。例如“您确定要删除这条记录吗此操作不可撤销”比“亲确定要删吗删了可就没了哦”更稳妥。这不代表文案要死板而是要求关键信息明确、操作后果清楚、按钮文字能表达动作本身比如“确认删除”而不是“OK”。写文案时多问一句“这句话不看上下文能不能懂”往往能筛掉很多低质量表达。7. 把可访问性做成长期机制而不是上线前的临时突击7.1 自动化工具能抓到七八成但抓不到全部单纯靠人工测试很容易遗漏所以自动化工具是第一步。我常用的组合是下面这些工具定位说明axe-core / axe DevTools自动化检测规则最丰富扫描页面给出 WCAG 相关失败项和修复建议适合集成到浏览器扩展和 CILighthouse页面质量总览有 Accessibility 分类得分直观但严格度不如 axe适合快速体检eslint-plugin-jsx-a11y / eslint-plugin-vuejs-accessibility源码级拦截在写代码阶段就提示缺少 alt、无效标签和可访问性陷阱WAVE可视化展示在浏览器上叠加图标显示结构问题适合给非开发同事做演示pa11y-ciCI 自动化回归把核心页面加入回归集每次发布前自动跑一遍我的建议是eslint 插件在前置阶段挡掉基础问题axe 在开发环境和 CI 里做深层检查Lighthouse 留下给团队快速了解页面整体情况。这些工具能抓到语义标签、对比度、alt 缺失等一大批问题但真正判断交互逻辑是否合理比如焦点管理和 ARIA 状态同步必须配合人工走查。7.2 一份够用的人工检查清单长期维护可访问性团队需要一个固定的人工检查流程时长不用太长。我的实战清单是这样纯键盘走查从首页开始用 Tab 完整走一遍主流程确认所有交互可见、可操作焦点顺序符合视觉阅读顺序开启读屏器抽查核心流程Windows 用 NVDAmacOS 用 VoiceOver检查关键页面对比度正文、按钮、错误提示各抽一组颜色验证用浏览器的“模拟缺失色觉”功能看几个核心页面确认状态不依赖单一颜色在系统设置里开启“减少动态效果”确认页面没有硬编码动画异常。这个过程单人 30 分钟可以完成比想象中轻量关键是形成周度或发布前必查的动作。最好的方式不是出一个几十条的完整报告而是把检查清单固定成团队模板命名成“发布前可访问性走查清单”每个人负责自己模块时顺手过一遍。7.3 从“项目补丁”走向“组件库规范”最后一步是在组件层做基建。可访问性如果每个业务页面单独做是极高的重复成本如果做进设计系统和组件库就等于全站受益。比如组件库里的 Button 组件默认支持键盘激活和 focus 样式Modal 组件内置焦点约束和 Esc 关闭Table 组件默认输出 th 和 scope这样业务开发者只要不自作聪明覆盖基本不会出大问题。在日常迭代中我在代码评审里加了一条硬性核对项新增交互是否支持键盘所有自定义组件是否有正确的可访问名称。包括我自己评审别人代码时也习惯先问三个问题能聚焦吗聚焦可见吗操作反馈可感知吗只要这三个答案都是“是”可访问性的地基基本就是稳的。如果团队能接受还可以安排每月一次小组走查用临时抽签的方式选一个页面大家一起走一遍效果比写一堆文档好得多。说到底前端可访问性不是某个“无障碍小组”的事它渗透在每个标签、每个事件、每个样式声明里。我个人的体会是做可访问性最值回票价的地方在于它逼着你去深入理解浏览器语义、焦点模型和用户真实的使用方式这些理解会反过来让普通用户的体验也变好。下次你的产品经理再问“可访问性要做多久”时你至少可以说先让我把 Tab 走一遍顺便过一遍 axe一小时之内我们就知道门到底哪里没开。别让网站对某些人关闭大门很多时候并不是很难的事关键是先认真走一遍用户要走的路。