tabindex仅用于控制聚焦能力而非排序,唯一合法值为0和-1;必须加tabindex="0"的是需键盘访问的非原生元素(如<div role="button">),原生控件(<button>、<input>)禁加;tabindex="-1"专供JS主动聚焦,非隐藏开关;正整数tabindex必须彻底删除,否则破坏焦点流与可访问性。

tabindex 不是用来给元素“排号”的,能用的只有 tabindex="0" 和 tabindex="-1";所有正整数(如 tabindex="1")都该立刻删掉,否则键盘用户和屏幕阅读器会同步迷失。
哪些元素必须加 tabindex="0",哪些绝对不能加
只有默认不可聚焦、但业务上又必须被键盘访问的容器才需要 tabindex="0",比如 <div role="button">、<h3 role="tab"> 或自定义折叠面板标题栏。原生可聚焦元素(<button>、<a href>、<input>)加了反而冗余,还可能干扰 React/Vue 的 hydration 或旧浏览器行为。
- 加了
tabindex="0"后,元素只是“能被 Tab 到”,不会自动响应 Enter/Space —— 必须监听keydown,且只对event.key === 'Enter'或event.key === ' '(注意是空格字符)做处理 - 必须同步设
role,例如role="button",否则屏幕阅读器读作“普通容器”,用户无法理解用途 - 纯装饰性元素(如图标
<span>、<svg>容器)加了tabindex="0"只会制造无效停靠点,应直接移除
tabindex="-1" 的唯一合法用途:让 JS 能 .focus(),但用户不能 Tab 到
它不是“隐藏开关”,而是动态组件中接管焦点的入口。模态框打开后调 element.focus() 却没反应?大概率是目标按钮是 <div>,没提前设 tabindex="-1";而原生 <button> 根本不需要它 —— 加了反而主动踢出 Tab 流。
- 模态框首次打开时,应立即对第一个可操作项(如“确认”按钮)调用
.focus(),该按钮需在 HTML 中就带tabindex="-1" - 元素若被
display: none或visibility: hidden隐藏,.focus()会静默失败,哪怕有tabindex="-1" -
focus({preventScroll: true})在 Safari 中不支持,传对象参数会失效;生产环境建议先检测:if ('preventScroll' in FocusOptions.prototype)
为什么 tabindex="1" 是最危险的写法
它不会让元素变成“第一个被 Tab 到的”,只会把它塞进所有正数里按升序排,且所有正数元素都会排在原生可聚焦元素之后 —— 导致导航路径断裂。更糟的是,在 React/Vue 动态渲染或 SSR hydration 场景下,这个值根本不可维护。
立即学习“前端免费学习笔记(深入)”;
- 多个
tabindex="1"元素中,浏览器只认 DOM 中第一个,其余被忽略 - 混用
tabindex="1"和tabindex="2"后,焦点顺序变成“正数升序 → DOM 顺序 →tabindex="0"元素”,完全脱离用户预期 - Flex/Grid 布局下视觉顺序与 DOM 顺序不一致时,正数
tabindex会让键盘流和视觉流彻底割裂 - 移动端 Safari 直接无视大部分
tabindex="0",tabindex="1"更无效,还可能触发控制台警告
焦点样式和隐藏逻辑不能只靠 CSS
用 outline: none 清掉默认轮廓后,若不补 :focus-visible,等于让键盘用户“看不见焦点在哪”——这是最常见也最致命的遗漏。同样,仅靠 opacity: 0 或 position: absolute; left: -9999px 隐藏的元素,仍可能被 Tab 到,必须显式加 tabindex="-1" 或现代 inert 属性。
-
:focus-visible只在键盘触发聚焦时生效,鼠标点击后自动退去,兼顾体验与可访问性 -
aria-hidden="true"不影响tabindex行为,若同时存在,必须显式设tabindex="-1",否则内部元素仍可能被 Tab 到 - 禁用状态优先用
disabled(对原生元素)或aria-disabled="true"(对自定义组件),别只靠灰掉样式 + 移除tabindex



















