tabindex只该用0和-1:必须加tabindex="0"的是需键盘访问的非原生元素(如),原生控件(等)加了反而干扰框架;tabindex="-1"专用于js主动聚焦,正整数会破坏焦点流且被wcag反对。

tabindex 只该用 0 和 -1,其他值基本等于埋雷——尤其正整数,在现代框架和屏幕阅读器中已普遍失效或引发不可预测的焦点跳转。
什么时候必须加 tabindex="0",什么时候加了反而坏事
它只对非原生可聚焦元素有意义:<div role="button">、<code><h3 role="tab"></h3>、自定义折叠面板标题这类“视觉上像按钮但本质是容器”的元素,不加 tabindex="0",键盘用户 Tab 到那里就直接跳过。
- 原生可聚焦元素(
<button></button>、<a href></a>、<input>、<select></select>、<textarea></textarea>)**绝对不要加**tabindex="0"——它们默认等效于tabindex="0",加了可能干扰 React/Vue 的 hydration 或焦点管理逻辑 - 纯展示容器(如
<div class="avatar"><img></div>、图标<span></span>、静态文本块)加了tabindex="0",只会让键盘用户多停一次却无事可做,还污染屏幕阅读器播报流 - 已包裹原生可聚焦子元素的卡片或列表项(比如内部有
<button></button>的<article></article>),外部再加tabindex="0"会导致双重焦点停靠,用户按一次 Tab 就卡住两次
tabindex="-1" 不是隐藏开关,是编程聚焦的唯一入口
设成 -1 后,元素不能被 Tab 键碰到,但能被 .focus() 主动聚焦——这是模态框、选项卡切换、动态表单流转的核心机制。
- 必须设在**具体可操作子元素**上,而不是整个容器。例如模态框打开后要聚焦第一个确认按钮,这个
<button></button>在 HTML 中就得带tabindex="-1";若设在<dialog></dialog>根节点上,.focus()会静默失败 - 异步加载的内容(如懒加载的表单字段)需确保 DOM 已挂载再调
.focus(),推荐用requestAnimationFrame(() => el.focus())或setTimeout(() => el.focus(), 0) - 聚焦后若目标被
overflow: hidden、transform或position: sticky遮挡,必须手动补el.scrollIntoView({ block: 'nearest' }),否则焦点进了但用户看不见 -
display: none或visibility: hidden的元素即使有tabindex="-1",.focus()也会静默失败
为什么永远不要写 tabindex="1" 及任何正整数
它不会让你“控制顺序”,只会把元素强行提到所有 tabindex="0" 元素之前,再按数值升序排——而多个 tabindex="1" 仍按 DOM 顺序走,和没设一样。更糟的是,一旦混入 tabindex="0" 的链接或按钮,Tab 流变成“正数 → 0 值 → 其他”,完全失控。
- 移动端 Safari 直接无视大部分
tabindex="0",tabindex="1"更无效,还可能触发警告 - React/Vue 动态插入新组件后,硬编码的正数无法重算,焦点跳转错乱
- 屏幕阅读器把第一个
tabindex="1"当作逻辑起点,如果它是页脚的“导出按钮”,用户一进页面就听到“导出”,主编辑区上下文全断 - WCAG 明确反对使用正整数
tabindex,实测中 90% 的键盘用户会因此迷失
真正难的不是写对值,而是判断“这个元素到底该不该进 Tab 流”——很多问题根源不在 tabindex 本身,而在语义缺失(没配 role)、交互未监听(没处理 keydown)、焦点样式被清空(没补 :focus-visible)。这三个点漏掉任意一个,tabindex="0" 就只是个半残的开关。











