tabindex只能用0和-1;必须加tabindex="0"的是本不可聚焦但需键盘访问的非原生元素(如),原生可聚焦元素(、等)绝对不能加,否则干扰焦点管理。

tabindex 只能用 0 和 -1,其他值(尤其是正数)会破坏键盘导航路径,且在 Safari、React 动态渲染、屏幕阅读器中表现不可靠。
哪些元素必须加 tabindex="0",哪些绝对不能加
只给「本不可聚焦但业务上必须支持键盘进入」的容器加 tabindex="0,比如 <div role="button">、<code><h3 role="tab"></h3>、自定义折叠面板标题栏。这些元素默认不会进 Tab 流,不加就键盘用户根本触达不了。
原生可聚焦元素(<button></button>、<a href></a>、<input>、<select></select>)**绝对不要加** tabindex="0"——它们天生就在 Tab 流里,加了反而可能干扰 SSR hydration 或 React 的焦点管理逻辑。
常见错误包括:
- 给纯展示文本或图标容器(如
<div class="avatar"><img></div>)加tabindex="0",键盘多停一次却无事可做 - 给已包含
<button></button>的卡片外层再加tabindex="0",导致双重焦点停靠 - 给
<nav></nav>或<main></main>这类语义容器加tabindex="0",既无交互意图,又污染焦点流
tabindex="-1" 不是隐藏开关,是编程聚焦入口
tabindex="-1" 的唯一合法用途:让一个具体可操作元素不被 Tab 键碰到,但允许 JS 主动调用 .focus() 聚焦它。它不是“禁用”,也不是“隐藏”。
典型场景:
- 模态框打开后,第一个按钮(如“确认”)必须在 HTML 中就带
tabindex="-1",JS 才能成功执行confirmBtn.focus() - 不要给整个
role="tabpanel"容器设tabindex="-1",否则内部所有<input>都失去自然聚焦能力 - 异步加载的表单字段,得等 DOM 渲染完成后再调
.focus();可用requestAnimationFrame(() => el.focus())避免静默失败 - 聚焦后若元素被
overflow: hidden或transform遮挡,必须手动补el.scrollIntoView({ block: 'nearest' })
为什么永远不要用 tabindex="1" 及更高正数
写 tabindex="1" 不会让它变成“第一个被 Tab 到的元素”,只会把它塞进所有正数里按数值升序排——但多个 tabindex="1" 仍按 DOM 顺序走,和没设一样。更糟的是,一旦混入 tabindex="0" 的链接或按钮,Tab 流变成“正数 → 0 值 → 其他”,完全失控。
真实影响包括:
- 移动端 Safari 直接无视大部分
tabindex="0",tabindex="1"更无效,还可能触发警告 - React/Vue 动态插入新组件后,硬编码的正数无法重算,焦点跳转错乱
- 屏幕阅读器把第一个
tabindex="1"当作逻辑起点,如果它是页脚的“导出按钮”,用户一进页面就听到“导出”,主编辑区上下文全丢 - Grid/Flex 布局中,正数 tabindex 无法对齐视觉顺序,浏览器只认源码位置
配套动作缺一不可:role、keydown、:focus-visible
加了 tabindex="0" 只解决“能不能被 Tab 到”,不解决“到了之后怎么用”和“焦点在哪看得见”。
必须同步做三件事:
- 加
role属性:比如role="button",否则屏幕阅读器读作“普通 div”,用户不知道这是可操作项 - 监听
keydown:只响应event.key === 'Enter'或event.key === ' '(注意是空格字符),且对空格要e.preventDefault(),否则页面滚动 - 补
:focus-visible样式:如果 CSS 移除了outline,必须写:focus-visible { outline: 2px solid #007bff; },否则焦点不可见;别用:focus,那会干扰鼠标用户
复杂点在于:tabindex 不控制方向键行为,也不决定语义层级。网格表格要用 role="grid" + role="row" + role="gridcell",评分量表这类结构必须回归 <table> + <code>aria-labelledby,仅靠 CSS 布局和 tabindex 无法传达行列关系。











