模态框焦点锁定原理是监听focusin事件,用modalel.contains(document.activeelement)判断焦点是否在内部,若跳出则阻止默认行为并聚焦首个可聚焦元素;tabindex="-1"仅支持js聚焦,aria-modal="true"仅作语义提示,不影响原生tab行为。

模态框内 Tab 键不跳出容器的原理
浏览器默认会把 Tab 键在所有可聚焦元素间线性遍历,包括模态框外的按钮、链接甚至 body。模态框要“锁住”焦点,本质是拦截焦点离开边界的行为,并主动把焦点跳回第一个或最后一个可聚焦子元素。
关键不是阻止 Tab,而是监听 focusin 事件,判断目标是否还在模态框内部;一旦发现焦点即将落到外部,立刻 event.preventDefault() 并手动 focus() 回去。
用 document.activeElement + contains() 判断焦点位置
这是最轻量、兼容性最好的判断方式:拿到当前获得焦点的元素,看它是不是模态框 DOM 节点的后代。
-
modalEl.contains(document.activeElement)返回true表示焦点仍在框内 - 注意:如果用户用鼠标点击了模态框外区域,
activeElement可能是body或input等,必须覆盖所有情况 - 不要只监听
keydown—— 用户可能用 Shift+Tab、鼠标点击、屏幕阅读器切换,focusin是唯一能捕获所有入口的事件
modalEl.addEventListener('focusin', (e) => {
if (!modalEl.contains(e.target)) {
const focusable = modalEl.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusable.length > 0) {
focusable[0].focus();
}
}
});
tabindex="-1" 和 aria-modal="true" 的实际作用
tabindex="-1" 只让元素可通过 JS .focus() 聚焦,但不会进入自然 Tab 顺序;它常用于模态框自身(避免被 Tab 到背景遮罩层),或临时聚焦某个非交互元素(比如标题)。
aria-modal="true" 是给辅助技术用的语义声明,告诉屏幕阅读器“当前只应操作这个模态框”,但它**完全不影响原生 Tab 行为** —— 浏览器照样会把焦点移出去。
- 别指望
aria-modal替你做焦点管理,它只是辅助提示 - 模态框根节点加
tabindex="-1"是好习惯,方便 JS 初始化时.focus()进去 - 遮罩层(overlay)建议设
tabindex="-1"并aria-hidden="true",否则可能被意外聚焦
循环焦点时第一个/最后一个元素怎么选
“循环”不是指按 Tab 就从末尾跳回开头,而是当焦点试图离开末尾时,跳回第一个;Shift+Tab 试图离开开头时,跳回末尾。关键在于识别两个边界点。
- 第一个可聚焦元素:通常用
querySelector找button, [href], input:not([type="hidden"]), [tabindex]:not([tabindex="-1"]) - 最后一个:用
querySelectorAll拿数组后取[length - 1],别用lastElementChild—— 它可能是文本节点或不可聚焦容器 - 动态内容(比如异步加载表单)需重新计算可聚焦元素列表,不能只初始化一次
- 如果模态框里没任何可聚焦元素,
focus()会失败,最好 fallback 到模态框自身(已设tabindex="-1")
焦点管理真正麻烦的从来不是“怎么跳”,而是“什么时候该跳”——比如弹窗嵌套、动态渲染、第三方组件插入 DOM 后自动聚焦。这些场景下,靠一次性初始化远远不够,得监听 DOM 变化或重置焦点逻辑。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











