只用 Tab 走到最后
一句话总结
键盘可访问性不是功能,而是顺序。什么能获得焦点,按什么次序循环,以及现在焦点在哪里是否看得见。这三点做到了,大部分问题就解决了。
为什么需要它
把鼠标移开,只用 Tab 键使用自己的页面 5 分钟,几乎总能发现问题。焦点会突然跳到页面最底部。进入一个隐藏起来的菜单之后,按多少次都出不来。看不出焦点现在在哪里。
这些问题在 QA 中永远发现不了,因为 QA 也是用鼠标做的。而且用户也不会上报——无法使用的页面,用户只会直接离开,而不是去上报。
只用键盘的人比想象的要多。手腕受伤的人、屏幕阅读器用户、因为手抖而难以用鼠标瞄准的人,还有单纯觉得 Tab 更快的人。如果这是支付页面,这些人就无法完成支付。
工作原理——顺序焦点导航顺序
HTML 标准的 tabindex 一节 规定的规则很简单。
1. tabindex 가 양수인 것들 — 값이 작은 것부터, 같으면 문서 순서
2. 그다음 tabindex="0" 과 기본으로 초점을 받는 요소들 — 문서 순서
该代码块中的韩文说明依次为:先是 tabindex 为正数的元素,按值从小到大,值相同则按文档顺序;然后是 tabindex 为 0 的元素和默认可获得焦点的元素,按文档顺序。
a[href]、button、input、select、textarea、summary 默认就能获得焦点。tabindex="-1" 只能通过脚本给予焦点,按 Tab 键不会到达。
在这里,人们最常吃亏的是正数 tabindex。tabindex="3" 并不是把这个元素单独挪到第三位,而是把整个页面分成两批。带正数的元素先全部循环一遍,然后才轮到其余元素。所以只要在某处加上 tabindex="1",这个元素就成了页面的第一站。
想改变顺序,就改变标记的顺序。不要用 tabindex 来移动。
只用 CSS 改变视觉顺序(order、grid-row)也会造成同样的问题。眼睛看到的次序与 Tab 循环的次序不一致,WCAG 的 Focus Order 所说的“保持含义的顺序”就被破坏了。
跳过链接
屏幕阅读器用户可以跳过地标,但只用键盘的用户做不到。 如果菜单有二十项,每个页面都要按二十次才能到达正文。
所以要在页面的第一站放一个通往正文的链接。WCAG 的 Bypass Blocks 要求的就是这个。
这里有两个陷阱。
- 必须是第一站。 如果放在菜单之后,要跳过的内容已经全都经过了
- 目标必须能获得焦点。 仅有
<main id="main">时,浏览器有时只会改变地址,焦点却原地不动。加上tabindex="-1",即使没有脚本,焦点也会跟过去
惯例是平时把它放在屏幕之外,只有焦点到达时才显示出来。.skip { position: absolute; left: -9999px } 和 .skip:focus { left: 8px } 这两行就够了。
隐藏的东西是否真的被隐藏了
折叠的菜单、关闭的对话框、被推到屏幕之外的面板。这些仍留在 Tab 顺序中的情况很常见。焦点会到达,但屏幕上什么也看不到,所以用户每按一次 Tab,都会觉得焦点消失了。
hidden属性或display: none——完全从焦点顺序中移除inert——其中的一切都不能获得焦点、不能点击、不能被读取aria-hidden="true"——只是从辅助技术的树中消失。焦点依然会到达
最后一种是最糟糕的组合。焦点会到达,屏幕阅读器却什么也不读。MDN 的 aria-hidden 文档 也明确规定不要用在可获得焦点的元素上。aria-hidden 只用于像用文字画出的图标那样既不需要读出、也不会获得焦点的内容。
焦点必须看得见
outline: none 是学 CSS 的人最先学会的一行,也是对可访问性破坏最大的一行。去掉焦点环之后,键盘用户就会在不知道自己在哪里的情况下继续按 Tab。
:focus-visible 解决了这个问题。用鼠标点击时不会触发,只有通过键盘到达时才会触发。这样既不会破坏设计,又能只对需要的人显示焦点环。
在现场相遇的样子
下面记录这个服务中实际发生过的三件事。
- 打开了模态框,焦点却没有进入模态框,屏幕阅读器仍然在读后面的正文
- 折叠的筛选面板不是
visibility: hidden而是高度为 0,里面的按钮依然留在 Tab 顺序中 - 设计系统的按钮组件全局设置了
outline: none,又没有提供替代样式,导致整个网站上都看不到焦点
这三件事只要“把鼠标移开,只用 Tab 键操作 5 分钟”,当场就能发现。把这一行加进 PR 检查清单,比十个自动工具都管用。
下一项实验要做什么
编写一个控制台页面,并用计算器确认按下 Tab 时实际经过的次序。把跳过链接放在第一站,去掉正数 tabindex,把折叠区域从 Tab 顺序中移除,并恢复焦点的显示。