路由跳转后必须手动重置焦点、广播变化、同步title、控制焦点流。需聚焦主标题(tabindex="-1")、用aria-live="polite"播报、及时更新document.title、模态框实现焦点陷阱与恢复,缺一不可。

路由跳转后焦点没落到新页面主标题
屏幕阅读器和键盘用户会卡在旧位置,以为页面没变。SPA 不刷新页面,浏览器不会自动重置焦点,必须手动干预。
每次路由匹配成功后,立刻执行 document.getElementById('main-heading')?.focus(),前提是该元素有 tabindex="-1"。不要用 scrollIntoView() 代替聚焦——它只滚动,不触发屏幕阅读器播报。
- 主标题是动态渲染的?得等 DOM 插入完成再聚焦,可用
requestAnimationFrame(() => focus())或 Vue 的nextTick、React 的useLayoutEffect - 别聚焦到
<div>或<span>上:它们默认不可聚焦,加了tabindex后又可能破坏语义;优先用<h1 tabindex="-1"> - 若主标题不存在(如某些空状态页),fallback 到
<main id="app" tabindex="-1">并确保其已挂载
内容变更没被屏幕阅读器感知
用户点了“订单列表”→“订单详情”,界面变了,但读屏软件毫无反应,相当于“静音切换”。这不是 JS 没执行,而是辅助技术根本没被通知。
在根容器(如 <main id="app">)顶部插入一个常驻的 <div aria-live="polite" aria-atomic="true"></div>。路由切换后,往这个区域写入语义化文本,例如:liveRegion.textContent = "已进入订单详情页面"。
立即学习“前端免费学习笔记(深入)”;
- 别用
aria-live="assertive"——它会打断当前播报,适合错误提示,不适合页面导航 - 确保该区域未被
display: none、visibility: hidden或aria-hidden="true"隐藏,否则辅助技术直接忽略 - 避免每次重 render 整个 live 区域;保持 DOM 节点静态存在,只更新
textContent或appendChild
document.title 没同步更新
浏览器标签页仍显示“首页”,用户无法通过任务栏快速识别当前页,书签也容易混乱。纯 HTML + JS 的 SPA 必须手写这行代码,不能依赖框架或模板自动更新。
每次路由处理函数执行时,同步设置 document.title = "订单详情 - 我的商城"。title 字符串里建议包含页面核心信息 + 品牌名,顺序按重要性从左到右(因为标签页截断通常发生在右侧)。
-
history.state为空(比如用户直接访问链接)时,仍要从 URL 解析出 title,不能跳过 - 若用
pushState或replaceState,记得第三个参数是 URL,title 参数在现代浏览器中被忽略,传空字符串即可 - 避免在异步回调(如 fetch.then)里才设 title,应放在路由匹配确认、DOM 渲染前就执行
模态框打开/关闭时焦点失控
弹窗出现后键盘焦点还在背景按钮上,用户按 Tab 就跳出弹窗;关掉后焦点丢失,得按多次 Tab 才回到主内容。这不只是体验问题,还违反 WCAG 2.1 标准。
模态框 show() 时,立即调用 firstFocusableElement.focus();hide() 时,必须恢复到打开前的焦点元素。
- 记录原焦点:在打开前保存
const prevActive = document.activeElement,关闭后执行prevActive?.focus() - 若原焦点元素已被销毁(如路由切换导致 DOM 替换),fallback 到
document.getElementById('main-heading')或#app - 禁用背景交互不能只靠
pointer-events: none——它不阻止键盘访问;得用inert属性,或遍历设tabindex="-1"
路由跳转可访问性的关键不在“有没有改 DOM”,而在于“有没有补全浏览器本该做的那几件事”:重置焦点、广播变化、更新上下文、控制焦点流。这些动作必须在 DOM 完全渲染后执行,且每个环节都可能因时机错位或语义缺失而失效。



















