tabindex="-1" 不能让元素获得初始焦点,因为它仅允许JavaScript主动调用.focus(),不参与Tab键顺序遍历;弹窗需显式调用focus()或聚焦首个可交互元素才能生效。

tabindex="-1" 为什么不能让元素获得初始焦点
因为 tabindex="-1" 只允许通过 JavaScript 主动调用 .focus() 聚焦,它不参与 Tab 键顺序遍历。弹窗打开时若仅靠 tabindex="-1" 设置在容器上,而没手动触发 .focus(),焦点仍留在背景页面——这是 Focus Trap 失效的第一步。
常见错误是这样写:
<div class="modal" tabindex="-1"> <button>确认</button> </div>
结果:Modal 显示后,键盘按 Tab 键直接跳到地址栏或页面其他可聚焦元素,完全绕过弹窗。
- 必须在 Modal 显示后,显式调用
modalElement.focus()(前提是它有tabindex="-1") - 如果 Modal 内第一个可交互元素是
<button>或<input>,更稳妥的做法是直接firstFocusable.focus() - 注意:
tabindex="-1"加在非可聚焦默认元素(如<div>)上才需要手动 focus;加在<button>上则它本身已可聚焦,无需额外设置 tabindex
如何正确实现 Focus Trap 的边界判断
Focus Trap 的核心不是“锁住”,而是监听 Tab 键并重定向焦点——当焦点即将离开弹窗最前/最后的可聚焦元素时,拦截事件并跳转到对端。
立即学习“前端免费学习笔记(深入)”;
关键陷阱在于:可聚焦元素列表不能只靠 querySelectorAll('[tabindex], button, input, select, textarea, a[href]') 硬编码,因为:
- 某些元素可能被
display: none或visibility: hidden隐藏,但仍匹配选择器 -
tabindex="-1"元素若未显式disabled或aria-hidden="true",会被纳入焦点流 - 动态渲染内容(如 React/Vue 组件)可能在 trap 初始化后才挂载,导致漏掉新元素
建议做法:
function getFocusableElements(modal) {
const candidates = modal.querySelectorAll(
'button:not([disabled]), input:not([disabled]), select:not([disabled]),
textarea:not([disabled]), a[href]:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'
);
return Array.from(candidates).filter(el =>
el.offsetParent !== null &&
window.getComputedStyle(el).visibility !== 'hidden' &&
window.getComputedStyle(el).display !== 'none'
);
}
Escape 键关闭 Modal 时焦点回退失效的根源
按下 Escape 关闭弹窗后,焦点没回到触发按钮(trigger),而是落到 document.body 或随机位置——这通常是因为没保存触发源,或恢复逻辑被异步操作打断。
典型错误写法:
button.addEventListener('click', () => {
modal.show();
// 没存 triggerEl
});
// Escape 处理里:
modal.addEventListener('keydown', e => {
if (e.key === 'Escape') {
modal.hide();
document.body.focus(); // ❌ 错误回退目标
}
});
正确做法:
- 点击触发按钮时,用
data-focus-return属性或闭包变量记录triggerEl - 隐藏 Modal 前,先检查
triggerEl是否还存在且可聚焦(DOM 可能已被移除或禁用) - 使用
setTimeout(() => triggerEl?.focus(), 0)确保 DOM 更新完成后再聚焦,避免因框架更新延迟导致 focus 失败
aria-modal="true" 和 inert 属性的兼容性取舍
aria-modal="true" 告诉屏幕阅读器“此元素外的内容应被忽略”,但它**不阻止键盘焦点进入背景**——它只是语义层隔离。真正阻断键盘焦点,仍需 Focus Trap + inert。
inert 是现代方案(Chrome 105+、Firefox 117+ 支持),但 Safari 目前无原生支持。所以实际项目中:
- 若需兼容旧浏览器,必须手写 Focus Trap + 手动禁用背景区域所有可聚焦子元素的
tabindex(临时设为-1) - 若只支持现代浏览器,可用
modal.inert = true简化逻辑,但别省略 Focus Trap —— 因为inert不影响已存在的焦点,用户可能正聚焦在背景输入框上,关闭弹窗后焦点不会自动转移 -
aria-modal="true"应始终与role="dialog"配合使用,且 Modal 必须有aria-labelledby或aria-label
最易被忽略的一点:Focus Trap 不是“一次配置永久生效”。Modal 显示/隐藏、内部 DOM 动态变化、甚至 CSS visibility 切换,都可能让焦点逃逸——trap 逻辑必须响应这些生命周期事件重新计算可聚焦元素。



















