弹出层焦点锁定需事件监听、主动聚焦与dom范围判断,仅设tabindex="-1"无效;首个可操作元素须加tabindex="-1"并手动管理tab循环及关闭后焦点回退。

弹出层(Modal)的焦点锁定不能靠 tabindex 单独实现,必须配合事件监听、主动聚焦和 DOM 范围判断——否则键盘用户一定会跳出 Modal,导致可访问性失败。
为什么给 Modal 根元素设 tabindex="-1" 没用
很多人以为在 <div class="modal"> 上加 <code>tabindex="-1" 就能“锁住焦点”,实际它只让这个容器自身可被 .focus() 调用,但完全不影响 Tab 键行为。更糟的是,它可能让容器抢走焦点,导致第一个按钮被跳过。
- 浏览器不会因父元素有
tabindex="-1"就禁止子元素进入 Tab 流 - 背景遮罩层(
<div class="modal-backdrop">)若没设 <code>tabindex="-1"且 DOM 位置靠前,有时真会被 Tab 到 - 移动端 Safari 在未触发用户手势前,对非原生可聚焦元素调用
.focus()会静默失败,此时tabindex="-1"存在但无效果 - 原生可聚焦元素(
<button></button>、<input>)默认可聚焦,但加tabindex="-1"是跨浏览器保险做法 - 不要加在标题、描述文字等非可操作元素上——它们不是响应点,加了反而让键盘用户停在空白处
- 若首个元素是自定义组件(如
<div role="button">),必须同时有 <code>tabindex="-1"和role="button",否则.focus()失效且语义缺失Tab 键循环必须手动拦截,
tabindex不参与计算浏览器不会自动把 Tab 键限制在 Modal 内。你得监听
focusin,检测焦点是否移出,再手动拉回。
anything-to-html下载将调研报告、方案对比、数据分析、需求梳理、会议纪要、设计说明、代码评审、学习笔记等任务产物输出为可在浏览器直接打开的单文件 HTML,替代 Markdown 作为最终交付物。Markdown 在信息密度、视觉清晰度和可分享性上已跟不上 AI 产物的丰富度,HTML 能同时承载排版、表格、SVG 等。
- 监听 Modal 根元素的
focusin事件,检查!modalEl.contains(e.target) - 若移出,立即获取所有可聚焦子元素:
modalEl.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])') - 聚焦第一个(Shift+Tab 时聚焦最后一个):
focusable[0].focus()或focusable[focusable.length - 1].focus() - 不要给 Modal 内所有元素统一加
tabindex="0"——这会引入多余停靠点,降低导航效率
关闭 Modal 后焦点必须回退到触发源
焦点不能消失或跳到
,否则键盘用户会迷失。这和tabindex的值无关,但依赖你在打开时记录触发元素。- 推荐方式:打开 Modal 前用
document.activeElement记录,关闭后调用triggerEl.focus() - 若触发源是动态生成的(如列表中某行的“编辑”按钮),建议传参保存引用,而非仅存 selector 字符串
- 注意:禁用的
<button disabled></button>自动脱离 Tab 流,无需额外处理tabindex,但需确保它仍是有效的 focus 目标(即未被display: none或inert掩盖)
真正容易被忽略的是:焦点陷阱不是一次设置就能一劳永逸的逻辑,它必须随 Modal 的 mount / unmount 生命周期同步管理,且在 SSR hydration 后、移动端首次用户手势前、以及 DOM 动态插入新控件时都可能失效——这些边界场景比基础实现更常出问题。
- 监听 Modal 根元素的
tabindex="-1" 必须加在首个可操作元素上
Modal 打开后,焦点要落到第一个可交互项(如“确认”按钮或 <input>),这个元素必须有 tabindex="-1",否则 .focus() 可能被忽略(尤其 SSR 渲染后 hydration 阶段)。










