tabindex="0"是表单项最该用的值,因其使元素按DOM顺序进入焦点流且无冗余;正数tabindex会破坏默认顺序,负数仅支持主动聚焦。

tabindex="0" 是表单项里最该用的值,不是“1”或“2”
表单项(input、select、button、textarea)默认就可聚焦,天然等价于 tabindex="0",手动加这个值是冗余但无害的;而给它们设 tabindex="1" 或 tabindex="2" 会强制插队,导致焦点跳到页脚又弹回顶部——不是你想“第一个聚焦”,而是浏览器把所有正数都提前排,再按数值升序挤进流里。
常见翻车点:
- 给多个
input都写tabindex="1",结果只有第一个生效,其余被忽略(浏览器只认最小正整数) - 在表单中间插入一个
tabindex="5"的div,后面所有原生控件全被压到它之后,Tab 顺序彻底错乱 - React/Vue 中动态渲染的表单项,若
tabindex写死为正数,DOM 插入顺序一变,焦点路径就不可预测
非原生表单项(如 div 模拟的开关)必须配 tabindex="0" + role
用 div 或 span 实现的自定义控件(比如带图标的切换开关),光加 tabindex="0" 不够——它能获得焦点,但按 Enter 或 Space 没反应,键盘用户卡住。
必须同步做三件事:
- 加
role="switch"(或role="checkbox"),让屏幕阅读器识别语义 - 监听
keydown,对event.key === 'Enter'和event.key === ' '(注意是空格字符)做相同处理,并对空格调用event.preventDefault()防止页面滚动 - 用
aria-checked或aria-pressed同步状态,否则辅助技术无法感知当前是否开启
想跳过某个表单项?用 tabindex="-1",不是删掉属性
禁用的 input[disabled] 或 button:disabled 自动退出焦点流,不用额外设 tabindex;但如果你只是临时隐藏逻辑、还想保留键盘操作入口(比如灰掉但允许回车激活),就不能用 disabled,而该用 aria-disabled="true" + tabindex="-1"。
tabindex="-1" 的真实作用是:不进 Tab 流,但能被 .focus() 主动聚焦。典型场景:
- 模态框打开后,立即将焦点移到关闭按钮(它需
tabindex="-1"才能被.focus()成功调用) - 表格行(
tr)设tabindex="0"获得焦点后,操作菜单按钮设tabindex="-1",等用户按 Tab 时再用 JS 把焦点传过去 - 表单提交失败后,自动聚焦到第一个出错的
input,哪怕它原本不在 Tab 流前端
移动端 Safari 对 tabindex 的限制比你想象中更硬
在 iOS Safari 上,未被用户主动点击/触碰过的页面,tabindex="0" 对非原生可聚焦元素(如 div)基本无效——.focus() 会被静默忽略,Tab 键也跳不过去。这不是 bug,是 Safari 的输入上下文安全策略。
应对方式很实际:
- 首次交互必须由用户触发(哪怕只是点一下空白处),之后
tabindex="0"才开始生效 - 避免依赖纯 JS 初始化焦点;改用用户动作(如点击按钮)作为起点,再调用
.focus() - 如果必须首屏聚焦,优先用原生
input或button,它们不受此限制
真正难的不是写对 tabindex,而是判断这个元素到底该不该进 Tab 流——有些“看起来像按钮”的东西,其实根本不该被键盘用户碰到。











