tabindex="-1" 是移出 Tab 流但保留脚本聚焦能力,仍可被 .focus() 主动聚焦,与 disabled 有本质区别;常用于模态框、下拉菜单、锚点跳转等需程序化接管焦点的场景。

tabindex="-1" 不是隐藏焦点,是移出 Tab 流但保留脚本聚焦能力
很多人看到 tabindex="-1" 就以为“这个元素现在不能被聚焦了”,其实完全相反:它依然能被 .focus() 主动聚焦,只是 Tab 键跳不过去。这跟 disabled 有本质区别——disabled 后按钮点不了、提交不了、.focus() 直接失败;而 tabindex="-1" 的按钮照样能点、能触发 click、能提交表单。
常见误用场景:
- 给
<button>加tabindex="-1"想“禁用它”,结果用户照样点、照样提交 - 用
display: none隐藏元素后还留着tabindex="-1",调用.focus()静默失败(因为不可见元素无法获得焦点) - 在模态框里设了
tabindex="-1"却没在打开时调用.focus(),键盘用户卡在背景上
哪些表单场景真需要 tabindex="-1"
核心原则:只在“需要程序化接管焦点入口”的时候用,且几乎只用于非原生可聚焦容器或临时锚点。
-
<div role="dialog">打开后,想让关闭按钮成为首个焦点项 → 关闭按钮必须带tabindex="-1",否则默认不可聚焦 - 下拉菜单容器(
<div class="dropdown">)本身不参与 Tab,但内部选项要支持方向键导航 → 容器设tabindex="-1",展开后再.focus()到第一个<button> - 页面内锚点跳转:
<section id="faq">是普通区块,location.hash跳转后想滚动+触发读屏 → 必须加tabindex="-1"才能element.focus() - 动态注入的表单项(如异步加载的地址选择器),首次渲染后需立即聚焦 → 元素挂载完成、且未被
inert或父级隐藏时,才可安全调用.focus()
focus() 失败的常见原因和绕过方式
不是浏览器不支持 .focus(),而是运行时条件没满足。
- 元素还没挂载到 DOM:React/Vue 中在
useEffect或mounted之后再调,别在模板里直接写ref.focus() - 父级用了
inert属性,或手动清除了所有子元素的tabindex→ 检查祖先节点是否阻断了焦点流 - 移动端 Safari 要求
.focus()必须在用户手势同步上下文中执行(比如click回调里),setTimeout或 Promise.then 里调会静默失败 - 焦点被其他逻辑抢走:比如 Modal 组件自带 focus trap,在你
.focus()后立刻又被重定向到首项 → 确保你的聚焦逻辑在 trap 启动之后、或暂时 disable trap
preventScroll 在 Safari 中无效,得降级处理
element.focus({preventScroll: true}) 在 Chrome 86+ 有效,但 Safari 完全忽略该参数(调用不报错,滚动照常发生)。
稳妥写法:
if ('preventScroll' in FocusOptions.prototype) {
element.focus({preventScroll: true});
} else {
element.focus();
}
注意:preventScroll 不解决元素不可见的问题——如果它被 display: none 或 visibility: hidden 隐藏,.focus() 仍会失败。真正要防跳闪,得配合滚动定位逻辑(比如 element.scrollIntoView({block: 'nearest'}))或 CSS scroll-behavior: smooth 控制。

















