弹出层打开时须用.focus()主动聚焦首个可操作元素,目标需设tabindex="-1";关闭后焦点必须返回触发元素;Tab键需拦截并手动循环聚焦以锁定焦点范围。

弹出层打开时怎么把焦点移到第一个可操作元素上
必须用 .focus() 主动触发,且目标元素得先有 tabindex="-1"。浏览器不会自动把焦点“推”进新出现的弹层,哪怕它 DOM 位置靠前。
常见错误是只给弹层容器加 tabindex="0",结果焦点停在空容器上,用户还得再按一次 Tab 才到按钮——这违反了 WCAG 2.1 的焦点管理要求。
- 模态框里第一个
<button></button>或<input>默认可聚焦,直接.focus()即可,不用设tabindex - 如果是自定义触发项(比如
<div role="button">),必须同时满足:<code>tabindex="-1"+role="button"+keydown监听 - 移动端 Safari 在未触发过用户手势前,
.focus()对非原生元素可能静默失败,建议搭配setTimeout延迟或监听click后立即调用 - 触发按钮是
<button></button>且未被销毁,直接保存其引用并复用.focus() - 如果触发源是动态渲染的(如列表中的某个“编辑”按钮),需在点击时传参或绑定 data-id,关闭后通过 selector 查找并聚焦
- 若触发元素已移除(如表格行被删除),焦点应回退到最近的逻辑父容器(如表格本身),而不是
body或document - 当焦点在最后一个可聚焦元素上且用户按
Tab,立即.focus()到第一个可聚焦元素 - 当焦点在第一个可聚焦元素上且用户按
Shift + Tab,立即.focus()到最后一个 - 使用
document.querySelectorAll('[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])')获取所有有效焦点项,避免漏掉自定义控件 - 给弹层标题加
tabindex="-1"是为了打开时能.focus()它,不是为了让它“第一个被 Tab 到” - 所有内部交互元素保持默认
tabindex(即不设,或设"0"仅用于非原生元素),不设正数 - 若弹层内容动态插入,务必等 DOM 更新完成后再执行焦点锁定逻辑,否则
querySelectorAll可能拿不到新元素
关闭弹出层后焦点该回哪
不能让焦点消失或跳到页面顶部,必须回到触发它的那个元素。否则键盘用户会丢失上下文,尤其在嵌套组件中极易卡死。
最稳妥的做法是在打开弹层前记录当前 document.activeElement,关闭时手动 .focus() 回去。别依赖 blur 事件——它不可靠,且可能被多次触发。
立即学习“前端免费学习笔记(深入)”;
怎么防止 Tab 键跳出弹出层范围
焦点锁定不是靠 CSS 隐藏或 tabindex 调整顺序实现的,而是拦截 Tab 键并手动控制焦点流向。仅靠 tabindex="-1" 或 inert 不足以阻止焦点逃逸。
标准做法是监听弹层内 keydown,捕获 Tab,判断焦点是否将移出边界,若是则阻止默认行为并手动聚焦到另一端:
为什么不能用正整数 tabindex 控制弹出层内部顺序
正整数 tabindex(如 "1"、"2")在弹出层里一样危险:它会让内部焦点流和外部文档流割裂,导致 SSR 渲染不一致、组件复用时顺序错乱,且屏幕阅读器无法按 DOM 线性理解。
弹出层内部也应严格遵循 DOM 顺序。你只需要确保结构合理:<h2></h2> 标题放最前,接着是 <input>,再是 <button></button>——它们天然就按这个顺序被 Tab 到。
.focus(),而是每次状态变化时都同步更新 aria-modal、aria-hidden 和 inert,并确保所有路径都被覆盖——比如 Esc 关闭、点击遮罩层关闭、以及浏览器后退按钮触发关闭。这些地方稍有遗漏,键盘用户就会被困住。



















