前端知库

Skip link 怎样实现?SPA 路由切换后焦点放到哪里?

Skip link 是页面上第一个可聚焦的链接,指向主内容,键盘用户按一次 Tab 再回车就能跳过导航,对应 WCAG 2.4.1 绕过区块(A 级)。它可以平时移出视口、获得焦点时显示,但不能用 display:none,否则无法聚焦。主内容容器加 tabindex="-1" 才能被脚本聚焦。SPA 切换路由时页面不重新加载,要把焦点移到新内容的标题或主区域上,并更新 document.title。

参考答案

简答

键盘用户打开一个网站,每个页面顶部都有几十个导航链接,想看正文就得一路按 Tab 穿过去。Skip link 就是放在页面最前面的一个“跳到主内容”链接,按一次 Tab 聚焦到它,回车后焦点直接落到主内容。

这对应 WCAG 2.4.1 绕过区块,是 A 级要求:在多个页面重复出现的内容块,比如导航、页头、广告,要有办法跳过。官方列出的充分技术里,第一个就是在每页顶部加一个直达主内容的链接;用 ARIA 地标或者标题标出各个区域,也属于充分技术。

实现上,链接写成 <a href="#main">跳到主内容</a>,放在 <body> 里第一个可聚焦的位置。W3C 建议它一直可见;只在获得焦点时显示也符合要求,所以常见写法是平时移出视口,:focus 时移回来。目标元素加 tabindex="-1",让它能被聚焦,但不会进入 Tab 顺序。

单页应用要多考虑一步。切换路由时页面不会重新加载,焦点可能还停在刚点的那个链接上,读屏用户也不知道页面已经换了。所以导航完成后,要更新 document.title,再把焦点移到新内容的 <h1> 或主区域上,同样给它加 tabindex="-1"。

一个 skip link 怎样写

下面是最小的写法。链接是 <body> 里第一个可聚焦元素,目标是 <main>:

HTML
<body>
  <a class="skip-link" href="#main">跳到主内容</a>
  <header>
    <nav><!-- 很多导航链接 --></nav>
  </header>
  <main id="main" tabindex="-1">
    <h1>订单列表</h1>
  </main>
</body>
CSS
.skip-link {
  position: absolute;
  left: 8px;
  top: -40px;
}

.skip-link:focus {
  top: 8px;
}

读者可能会问,为什么不直接 display: none,聚焦时再显示?因为 HTML 规范要求可聚焦的元素必须正在被渲染,display: none 的元素没有被渲染,不能获得焦点,用户按 Tab 根本到不了它;MDN 也说明它会被移出无障碍树。把它移出视口,元素仍然可以聚焦,聚焦时再移回来就看得见了。

W3C 的 G1 技术建议这个链接最好一直可见,因为切换设备用户、屏幕放大用户、语音控制用户都能从中受益;不过 2.4.1 本身只要求它在获得键盘焦点时可见。

<main> 上的 tabindex="-1" 是为了让它能被聚焦。MDN 的说明是,负值的 tabindex 表示元素不能通过 Tab 顺序到达,但可以用 focus() 方法聚焦。所以它不会在 Tab 顺序里多出一个停留点。不要用正数 tabindex 把链接排到前面,MDN 明确建议只用 0 和 -1,正数会打乱键盘用户的操作顺序。

怎样验收

G1 的测试步骤可以直接拿来用:页面上第一个可聚焦的控件是一个链接;链接文字说明它通向主内容;链接一直可见,或者获得键盘焦点时可见;激活链接后,焦点移到主内容;之后键盘焦点停留在主内容里,再按 Tab 会到达主内容里的下一个可交互元素。

这几步可以放进整页的键盘验收里一起做,见《怎样验证一个页面可以只用键盘操作?》。

SPA 切换路由后焦点放到哪里

传统多页网站点链接后整页重新加载,焦点回到页面开头,读屏软件会读出新页面。单页应用只是替换了一部分 DOM,这些都不会自动发生。焦点可能停在被点击的导航链接上,甚至停在一个已经被删掉的元素上。

常见的做法是在路由切换完成后做两件事:

JavaScript
function onRouteChange(title) {
  document.title = title;
  const heading = document.querySelector('main h1');
  heading?.focus();
}

前提是这个 <h1> 也带 tabindex="-1",否则普通的标题元素不能被脚本聚焦。焦点落在标题上,读屏软件会读出新页面的标题,键盘用户接着按 Tab 就从新内容开始。路由层面还要处理哪些事,比如滚动位置和数据加载,见《前端路由除切换组件外还需要管理什么?》。

聚焦标题之外,也可以用 aria-live 区域播报“已进入订单列表”,见《aria-live 怎样用于动态状态播报?》。两种方式可以一起用,但聚焦解决的是“下一步从哪里开始操作”,播报只解决“知道页面变了”。

常见追问

页面已经用了 <main>、<nav> 这些地标,还需要 skip link 吗?

从 2.4.1 的角度看,ARIA 地标和标题都属于官方列出的充分技术,读屏用户可以按地标或标题跳转。但没有读屏软件的键盘用户用不了这些快捷方式,所以很多网站两者都做。

导航在页面底部,也要加 skip link 吗?

不一定。官方理解文档的说明是,2.4.1 关心的是在到达主内容之前要穿过的重复内容;导航放在页面底部时,可能就不需要跳过链接。

tabindex="-1" 能加到任意元素上吗?

能加,但目的要明确。它适合“需要被脚本聚焦、但不该出现在 Tab 顺序里”的元素,比如主内容容器、新页面标题、出错时要聚焦的提示。不要用它或者 tabindex="0" 把 <div> 伪装成按钮,MDN 建议直接用 <button>、<a> 这类原生交互元素。