焦点锁定不能仅靠tabindex实现,必须结合js事件监听与主动聚焦逻辑;tabindex="-1"需加在首个可操作元素上,关闭后须将焦点回退至触发源。

弹出层(Modal)打开后,焦点必须被限制在内部,否则键盘用户会意外跳到背景内容上——tabindex 本身不实现“锁定”,它只是配合 JS 焦点管理的必要基础。
为什么不能只靠 tabindex 锁焦点
tabindex 是单个元素的属性,不是容器级开关。给 Modal 根元素设 tabindex="-1" 只让它自身可被 .focus() 调用,但不会阻止 Tab 键离开 Modal;真正起作用的是事件监听 + 主动聚焦逻辑。
- 仅设置
tabindex="-1"在 Modal 容器上,对焦点锁定无效——它甚至可能让容器抢走焦点,导致第一个按钮反而被跳过 - 背景遮罩层(如
<div class="modal-backdrop">)若没设 <code>tabindex="-1",又没禁用 pointer-events,有时会被 Tab 到(尤其当它在 DOM 中位置靠前时) - 移动端 Safari 在未触发用户手势前,
.focus()对非原生可聚焦元素静默失败,此时tabindex="-1"存在但无实际效果 - 原生可聚焦元素(
<button></button>,<input>)默认可被.focus(),但加tabindex="-1"是保险做法,确保跨浏览器行为一致 - 不要加在 Modal 标题或描述文字上——它们不是可操作项,加了反而让键盘用户停在不可响应的位置
- 若首个元素是自定义组件(如
<div role="button">),则必须同时有 <code>tabindex="-1"和role="button",否则.focus()失效且语义缺失Tab 键循环需手动拦截,
tabindex不参与计算浏览器不会自动把 Tab 键限制在 Modal 内;你得监听
focusin事件,检测焦点是否移出 Modal,并手动拉回。- 监听 Modal 根元素的
focusin,检查e.target是否属于 Modal 内部:!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() - 注意:不要用
tabindex="0"给 Modal 内所有元素“统一加”,这会引入多余停靠点,降低导航效率
关闭 Modal 后焦点必须回退到触发源
焦点不能消失或跳到
,否则键盘用户会迷失。这和tabindex的值无关,但依赖你在打开时记录触发元素。- 打开 Modal 前,保存
document.activeElement,或显式传入触发按钮引用(如openModal(triggerBtn)) - 关闭后,调用
triggerBtn.focus()—— 若触发元素已销毁或不可聚焦(比如是 disabled 的<button></button>),需 fallback 到最近的可聚焦祖先 -
tabindex="-1"在触发元素上不是必需的,但如果它本身是<div> 类自定义控件,则必须有 <code>tabindex="0"(打开前)和tabindex="-1"(关闭后临时移除?不,保持即可)才能保证回退有效真正容易被忽略的是:焦点管理不是一次性配置,而是状态变化时的连续动作——Modal 打开、内容动态加载、内部 Tab 键流转、关闭、触发源变更,每个环节都可能让
tabindex的值失去上下文意义。别把它当成 CSS 属性去“设置完就不管”。
- 监听 Modal 根元素的
tabindex="-1" 必须加在 Modal 内首个可操作元素上
Modal 打开后,焦点要落到第一个可交互项(如“确认”按钮或输入框),这个元素必须有 tabindex="-1",否则 .focus() 调用可能被忽略(尤其在 SSR 渲染或 hydration 后)。
将调研报告、方案对比、数据分析、需求梳理、会议纪要、设计说明、代码评审、学习笔记等任务产物输出为可在浏览器直接打开的单文件 HTML,替代 Markdown 作为最终交付物。Markdown 在信息密度、视觉清晰度和可分享性上已跟不上 AI 产物的丰富度,HTML 能同时承载排版、表格、SVG 等。










