我做前端这些年,真正把前端可访问性(Accessibility)当回事,是源于一次用户反馈。有个“校友资料查询”的功能上线,产品经理转述一位用户的疑问:为什么列表里的姓名用 Tab 键选不中。我们当时查了半天代码,按钮、链接都是正常写的,直到我用键盘走查才发现,那条列表项的“查看详情”入口是一个自定义 div 加 click 事件,鼠标能点,键盘却始终无法聚焦,读屏用户自然也无从入手。这个功能本质上就是:对于某些人,它的门是关着的。
这让我意识到一个问题:前端可访问性从来不是一个“锦上添花”的加分项,它决定了一个功能到底面向哪些人,又把哪些人挡在门外。这篇内容想从一个前端开发者的视角,把可访问性这件事拆开讲清楚——它服务的对象到底是谁、最常见的坑出现在哪里、应该如何修,以及如何把检查融入日常开发流程,而不是每次上线前才临时突击。适合前端开发者、组件库维护者,也适合那些隐隐觉得产品“对某些用户不太友好”但不知道从哪抓起的同学。
1. 重新认识被关在门外的人:可访问性不仅是“给盲人做优化”
1.1 一张用户谱系图:永久、临时、情境性障碍
很多人一提到可访问性,第一反应是“给盲人读屏用的”。这个刻板印象没错,但把范围窄化了。无障碍领域常用一个用户谱系来展示受益人群:永久性障碍,比如失明、失聪、行动不便;临时性障碍,比如手臂骨折、眼睛手术后暂时模糊;情境性障碍,比如户外强光下看不清屏幕、开会时不能开声音、双手抱娃只能用单手操作、甚至只是今天忘带耳机但在地铁上想看视频。
对一个互联网产品来说,今天访问你网站的大部分用户可能没有任何障碍,但几乎每个用户一生中都会经历至少一次临时性障碍或情境性障碍。从这个角度看,可访问性提升的不只是残障群体的体验,而是所有人的体验上限。比如表单里的 label 与输入框正确关联,读屏用户能读出“用户名”,普通用户点击文字也能聚焦输入框;视频字幕最初的服务对象是听障用户,但最后受益的是在通勤地铁上不带耳机的所有人。这个思维转换很重要,它决定了你做可访问性是“应付标准”还是“顺势把产品做好”。
1.2 商业、法律与职业层面的现实驱动力
除了人文视角,商业层面的理由也很具体。全球有相当比例的成年人存在某种程度的视觉障碍或其他障碍,他们同样有消费力、同样会被产品体验影响决策。对面向公众的产品来说,可访问性改进直接带来的是客服投诉减少、用户转化路径更顺畅;而语义化结构对搜索引擎也更友好,机器友好的内容通常对人更友好。
对前端开发者个人来说,可访问性技能在面试和团队评审中的分量越来越重。这几年我面试前端岗位,必问的一道题是“如果用户用键盘而不是鼠标操作你的页面,有哪些功能会不可用?”能深入回答的人,往往对交互细节有极强的掌控力,写出来的代码也更有结构性。这个技能不会过时,因为用户群体的多样性和法律法规的强制性都在推着行业往前走,提前掌握的人只会越来越占优势。
2. 键盘可达性是最低门槛:从一次 Tab 走查排障讲起
2.1 为什么先看键盘?因为鼠标不是唯一的指针
WCAG(Web Content Accessibility Guidelines)把“可操作”作为四大原则之一,而键盘可达性是最直接的操作性指标。原因非常简单:屏幕阅读器用户、部分运动障碍用户、重度快捷键用户都不依赖鼠标。如果一个交互无法用键盘完成,对这些人来说它就是完全不存在的。
开发中常见的键盘不可达问题主要有三类:
- 自定义元素没有 tabindex,焦点根本没有进入可聚焦序列;
- 有 tabindex,但操作事件只绑定了 click,没有监听键盘事件;
- 焦点进去了出不来,也就是“焦点陷阱”,最常见于弹窗、日期选择器这类浮层组件。
这三种问题我几乎每周都能在别人的代码里看到,自己早期也踩过不止一次。除非团队里有人专门负责可访问性审查,否则这类问题很容易在自测时被忽略——因为绝大多数开发者的日常操作都是鼠标优先。
2.2 一次实际的走查排障过程
具体排查方法并不复杂,养成习惯是关键,就是一个一个按 Tab。以我经历过的“查询结果列表不可键盘访问”为例,完整链路是:
- 打开页面,确认焦点起始位置;
- 连续按 Tab,观察焦点是否按预期顺序在搜索框、按钮、列表、翻页控件之间移动;
- 走到某个“查看详情”入口时,焦点直接跳过,因为它是一个 div,没有 tabindex;
- 这时不要急着加 tabindex,先找产品确认这个入口的语义。它的行为是触发一个查看动作,本质就是一个按钮;
- 改法是把 div 换成 button 标签,而不是硬加 tabindex;
- 换完后 Tab 能聚焦了,但按回车没反应,因为 click 事件原来写在 div 的 onClick 上,换成 button 后验证原生 Enter 是否能触发 click;
- 确认触发逻辑无误,再检查焦点样式是否可见,最后回归一遍主流程。
这里要特别注意两点。第一,不要给所有 div 都塞 tabindex="0" 让它们变成可聚焦。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 scope="col"或scope="row",读屏用户按行列浏览时能知道当前单元格对应的字段是什么。很多人习惯用 td 然后把样式加粗,这在视觉上没问题,但对读屏用户来说,表头和数据之间的关联就断了。表格如果很复杂,还可以用 caption 给表格一个标题,让读屏用户先知道这张表讲的是什么。
4. ARIA 的正确打开方式:能不用就不用,要用就用到点子上
4.1 ARIA 不是万能膏药
ARIA(Accessible Rich Internet Applications)是一套补充语义的属性体系,但它并不能改变浏览器原生行为的细节。社区有句话叫“ARIA 的第一规则是不要用 ARIA,除非原生语义真的无法覆盖”。滥用 ARIA 的典型场景包括:
- 给原生 button 加 role="button",原生的本来就是 button,加了等于加了一块遮羞布,有时反而让屏幕阅读器重复播报;
- 给 div 加 role="button" 却只处理 click,键盘支持没有真正补全;
- 用 aria-label 覆盖可见文本但文案不一致,读屏用户听到的和弱视用户看到的是两套信息,会产生矛盾。
正确的思路是优先原生 HTML,原生覆盖不了的交互模式,比如自定义弹窗、手风琴、菜单、滑块,才需要 ARIA 做辅助。ARIA 能做的只是“补充说明”,不是“重塑交互”。
4.2 弹窗、菜单、手风琴的处理套路
以弹窗为例,一个相对完整的可访问性方案包括:role="dialog" 加 aria-modal="true" 标明模态对话框;aria-labelledby 指向弹窗标题 id,读屏用户一进入就知道这个弹窗是干什么的;打开弹窗后焦点移入弹窗;焦点约束在弹窗内部,Tab 和 Shift+Tab 不能跑到页面背景里;关闭后焦点回到打开弹窗的按钮上;按 Esc 关闭弹窗并恢复焦点。
这套规则里最容易漏掉的是“关闭后焦点回到入口”和“Esc 关闭”。前者对键盘走查用户来说特别明显——关完弹窗焦点还停在 body 上,下一次 Tab 直接跳到页面开头,跳跃感很强。菜单组件要注意展开收起时 aria-expanded 状态的同步切换;手风琴内容在展开前不应该让读屏读到大段隐藏文本,通常用 hidden 属性或对隐藏元素做处理。这些不是标准答案,但它们是过去二十年社区踩坑沉淀出来的通用模式,照着做不会错。
4.3 aria-live:让动态内容“说人话”
页面里异步加载的内容、错误提示、表单校验信息,如果不处理,读屏用户完全不知道页面已经变了。这时候需要 aria-live 区域:aria-live="polite" 适合普通通知,aria-live="assertive" 适合紧急错误。但使用时要有节制,实时滚动的时间更新、频繁的数值变化不要一直“说话”,否则会干扰读屏用户获取信息。
我最常用的是错误提示容器设置 role="alert",它本质上是 assertive live 区域。提交表单校验失败时,把错误信息塞进这个容器,读屏用户就能立刻听到。这里有个坑:如果容器本身是在页面加载后动态创建的,某些读屏可能来不及建立链接。稳妥做法是页面初始渲染时就放一个空的 role="alert" 容器,后续只改它的文本内容,这样大多数读屏都能稳定播报。这个细节属于“不踩一次根本不知道存在”的问题,所以组件库代码里如果提前内置了,业务方会很省心。
5. 颜色、对比度和动效:视觉层面的那些“看不见”的门槛
5.1 对比度不是“能看清”就行
WCAG 2.x 对文本对比度的要求是:普通正文至少 4.5:1(AA),大字或加粗至少 3:1,UI 组件如输入框边框、焦点指示也建议达到 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 相关失败项和修复建议,适合集成到浏览器扩展和 CI |
| Lighthouse | 页面质量总览 | 有 Accessibility 分类得分,直观但严格度不如 axe,适合快速体检 |
| eslint-plugin-jsx-a11y / eslint-plugin-vuejs-accessibility | 源码级拦截 | 在写代码阶段就提示缺少 alt、无效标签和可访问性陷阱 |
| WAVE | 可视化展示 | 在浏览器上叠加图标显示结构问题,适合给非开发同事做演示 |
| pa11y-ci | CI 自动化回归 | 把核心页面加入回归集,每次发布前自动跑一遍 |
我的建议是:eslint 插件在前置阶段挡掉基础问题,axe 在开发环境和 CI 里做深层检查,Lighthouse 留下给团队快速了解页面整体情况。这些工具能抓到语义标签、对比度、alt 缺失等一大批问题,但真正判断交互逻辑是否合理,比如焦点管理和 ARIA 状态同步,必须配合人工走查。
7.2 一份够用的人工检查清单
长期维护可访问性,团队需要一个固定的人工检查流程,时长不用太长。我的实战清单是这样:
- 纯键盘走查:从首页开始,用 Tab 完整走一遍主流程,确认所有交互可见、可操作,焦点顺序符合视觉阅读顺序;
- 开启读屏器抽查核心流程:Windows 用 NVDA,macOS 用 VoiceOver;
- 检查关键页面对比度:正文、按钮、错误提示各抽一组颜色验证;
- 用浏览器的“模拟缺失色觉”功能看几个核心页面,确认状态不依赖单一颜色;
- 在系统设置里开启“减少动态效果”,确认页面没有硬编码动画异常。
这个过程单人 30 分钟可以完成,比想象中轻量,关键是形成周度或发布前必查的动作。最好的方式不是出一个几十条的完整报告,而是把检查清单固定成团队模板,命名成“发布前可访问性走查清单”,每个人负责自己模块时顺手过一遍。
7.3 从“项目补丁”走向“组件库规范”
最后一步是在组件层做基建。可访问性如果每个业务页面单独做,是极高的重复成本;如果做进设计系统和组件库,就等于全站受益。比如组件库里的 Button 组件默认支持键盘激活和 focus 样式,Modal 组件内置焦点约束和 Esc 关闭,Table 组件默认输出 th 和 scope,这样业务开发者只要不自作聪明覆盖,基本不会出大问题。
在日常迭代中,我在代码评审里加了一条硬性核对项:新增交互是否支持键盘;所有自定义组件是否有正确的可访问名称。包括我自己评审别人代码时也习惯先问三个问题:能聚焦吗?聚焦可见吗?操作反馈可感知吗?只要这三个答案都是“是”,可访问性的地基基本就是稳的。如果团队能接受,还可以安排每月一次小组走查,用临时抽签的方式选一个页面,大家一起走一遍,效果比写一堆文档好得多。
说到底,前端可访问性不是某个“无障碍小组”的事,它渗透在每个标签、每个事件、每个样式声明里。我个人的体会是,做可访问性最值回票价的地方在于:它逼着你去深入理解浏览器语义、焦点模型和用户真实的使用方式,这些理解会反过来让普通用户的体验也变好。下次你的产品经理再问“可访问性要做多久”时,你至少可以说:先让我把 Tab 走一遍,顺便过一遍 axe,一小时之内我们就知道门到底哪里没开。别让网站对某些人关闭大门,很多时候并不是很难的事,关键是先认真走一遍用户要走的路。