防止焦点在模态框外泄露的关键是正确建立并维护可控焦点边界,需四步闭环:打开时立即聚焦首个可聚焦元素;Tab键循环限制在模态框内;背景内容设inert或polyfill禁用交互;关闭后将焦点返回触发源或最近可聚焦祖先。

防止焦点在模态框外泄露,关键不是“避免陷阱”,而是**正确建立并维护一个可控的焦点边界**。焦点逃逸本质是边界没封住、初始没捕获、退出没归还——三者缺一不可。
锁定焦点入口:打开时立即接管
模态框显示后,必须在 DOM 渲染完成的下一帧(如用 setTimeout(..., 0) 或 requestAnimationFrame)将焦点设到第一个可聚焦元素上。不能依赖 autofocus 属性,它不可靠且可能被浏览器忽略或与其他组件冲突。
- 推荐目标:关闭按钮(
)或首个输入框,确保语义清晰、位置合理 - 若首个元素被禁用或隐藏,需跳过,动态查找第一个 可见、启用、可聚焦 的元素(可用
getTabables()工具函数辅助)
围住焦点出口:限制 Tab 循环范围
仅靠初始聚焦不够。用户按 Tab 键时,焦点必须在模态框内循环,不能流到背景按钮、导航栏或页脚。
- 监听模态框容器的 keydown 事件,只响应
Tab键(区分shiftKey) - 提前收集所有可聚焦子元素:
a[href], button:enabled, input:enabled, textarea:enabled, select:enabled, [tabindex]:not([tabindex="-1"]) - 当焦点在最后一个元素且按 Tab → 聚焦第一个;在第一个且按 Shift+Tab → 聚焦最后一个
- 调用 event.preventDefault() 阻止默认跳转行为,这是防止逃逸的强制手段
隔绝外部干扰:让背景内容“失活”
光锁住焦点还不够——背景元素仍可能被点击、被屏幕阅读器读取,甚至意外触发交互。最稳妥的方式是让它们“不可聚焦、不可交互”。
- 现代方案:给模态框外的根容器(如
或主内容区)添加 inert 属性,浏览器原生支持该语义(Chrome 105+、Firefox 122+、Safari 18.1+) - 兼容旧版:引入 inert polyfill(如 Google 提供的),或手动遍历并设置
tabindex="-1"+aria-hidden="true"+ 禁用 pointer-events - 注意:不要只靠 CSS
visibility: hidden或display: none,它们不阻止键盘聚焦
收尾不丢焦点:关闭后必须回传
模态框关闭瞬间,焦点若落在 document.body 或丢失,用户会迷失位置,尤其对屏幕阅读器用户极不友好。
- 打开前记录触发源:
const trigger = document.activeElement,或显式传入触发按钮引用 - 关闭后立即执行
trigger.focus();若触发源已销毁(如按钮被移除),则退回到逻辑上最近的可聚焦祖先(如导航菜单项、页面标题等) - React 框架中使用 returnFocus 属性(如 React Focus Lock、MUI、BootstrapVue)可自动处理,但务必显式启用,勿依赖默认值
不复杂但容易忽略——真正起作用的,是这四步闭环:进得来、转得开、出不去、回得去。










