模态框焦点锁定需手动实现:捕获tab键,计算首末可聚焦元素并重定向焦点,首次打开时主动focus首个元素,关闭后恢复触发按钮焦点,同时动态管理aria-hidden和aria属性以保障可访问性。

模态框打开后按 Tab 键焦点跑出遮罩怎么办
焦点锁定不是自动发生的,必须手动干预。浏览器默认会按 DOM 顺序循环 tabindex 可聚焦元素,模态框外的按钮、链接仍可被访问到。
核心做法是:捕获 Tab 键事件,计算模态框内第一个和最后一个可聚焦元素,当焦点即将移出范围时,强制重定向到对端元素。
- 先用
querySelectorAll收集所有可聚焦元素:"button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])" - 过滤掉
display: none或visibility: hidden的元素(仅靠 CSS 隐藏不等于不可聚焦) - 监听
keydown事件,判断e.key === 'Tab',再根据document.activeElement位置做跳转 - 注意首次打开模态框时,要主动
focus()到第一个可聚焦子元素(比如确认按钮),否则键盘用户无从开始
aria-modal="true" 和 role="dialog" 为什么不能替代焦点管理
aria-modal="true" 告诉屏幕阅读器“当前模态框是唯一可交互区域”,但它不阻止键盘焦点离开,也不影响浏览器原生 tab 导航逻辑。Chrome 和 Safari 目前都未实现该属性的焦点隔离行为。
实际测试中,即使设置了 role="dialog" 和 aria-modal="true",Tab 依然能落到背景内容上——这对视障用户是严重障碍,因为屏幕阅读器可能读出模态框外的上下文,造成混淆。
-
aria-modal是辅助技术提示,不是运行时约束 - 必须搭配 JavaScript 焦点锁定,二者缺一不可
- 不要省略
aria-labelledby和aria-describedby,否则屏幕阅读器无法获取模态框标题和说明
遮罩层 div[aria-hidden="true"] 被误读或漏读的问题
给背景内容加 aria-hidden="true" 是常见做法,但容易出错:如果模态框是动态插入的,而背景 DOM 结构在之后又通过框架(如 React)重新渲染,aria-hidden 可能被覆盖或丢失。
更稳妥的方式是:只对模态框父容器以外的全部 body > * 元素批量设置 aria-hidden="true",并在模态框关闭时还原——而不是依赖静态 HTML 属性。
- 避免对
body直接设aria-hidden,它会影响整个页面,包括模态框自身 - 若使用 Vue/React,别在组件挂载时硬编码
aria-hidden,应通过状态驱动属性更新 - 测试时用 NVDA + Firefox 或 VoiceOver + Safari,检查是否还有背景文字被朗读
ESC 关闭后焦点没回到触发按钮的后果
用户按 ESC 关闭模态框后,焦点若停留在 body 或丢失,下一次按 Tab 会从页面顶部开始,破坏操作流。尤其对键盘用户,这意味着要多按好几次 Tab 才能回到原来位置。
解决方法很简单:在模态框关闭逻辑里,显式调用触发按钮的 .focus()。前提是保存了该按钮的引用(比如点击时存到 data-trigger 或闭包变量中)。
- 不要用
document.activeElement.blur()后不恢复,这是常见疏忽 - 若触发源是
@#@#@#@#@#@#@#@#@#@0这类非按钮元素,确保它有tabindex="0"且可被focus() - 移动端 Safari 对
focus()支持不稳定,可加setTimeout(..., 0)微任务延迟执行
可访问性不是加几个 ARIA 属性就完事的事。焦点管理链条里任何一环断裂——首次聚焦、循环锁定、ESC 恢复、ARIA 同步——都会让键盘或屏幕阅读器用户卡住。最常被忽略的是“关闭后焦点归位”和“动态 DOM 更新时 aria-hidden 的同步”。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











