表单项 tab 顺序应依赖 dom 顺序或 tabindex="0",禁用正数 tabindex;原生表单控件默认可聚焦,无需额外设置;视觉与 dom 顺序不一致时应重构 html 或调整 css order;仅对非原生交互元素设 tabindex="0" 并补全语义与键盘支持。

表单项的 Tab 顺序不该靠 tabindex="1"、tabindex="2" 硬排——它会绕过按钮、跳过禁用字段、在 Safari 和动态渲染中彻底失效。真正可控的方式只有两种:tabindex="0"(补位)和不设(依赖 DOM 顺序),其余都是陷阱。
input 默认就在 Tab 流里,别画蛇添足
原生 <input>、<button></button>、<select></select>、<textarea></textarea> 默认等价于 tabindex="0",浏览器按 HTML 源码顺序自然聚焦。给它们加 tabindex="0" 或正数,纯属冗余甚至有害:
- React/Vue 中组件重渲染后 DOM 顺序变化,硬编码的
tabindex="2"会让焦点跳到错误位置 - Safari 对正数
tabindex支持不稳定,尤其在表单嵌套或 iframe 场景下常静默忽略 -
disabled的<input disabled>自动退出 Tab 流,比手动设tabindex="-1"更干净可靠 - 加了
tabindex="0"的<button></button>可能干扰 SSR hydration 阶段的焦点恢复逻辑
视觉顺序 ≠ DOM 顺序时,优先重构 HTML
如果设计稿是“右栏先填、左栏后填”,但 HTML 是左→右写的,不要给每个 <input> 加 tabindex="1"、tabindex="2"。这种写法会让中间的 <button></button> 被跳过,且无法响应屏幕阅读器的线性阅读流。
- 用 CSS Grid 或 Flex 的
order属性调整视觉位置,保留 HTML 语义顺序 - 必要时拆分表单结构:把右栏字段提前写在左栏之前,哪怕视觉上它在右侧
- 若必须用 JS 动态插入字段,确保插入后调用
element.focus()并配scrollIntoView(),而不是依赖tabindex排序
需要干预时,只对非原生控件用 tabindex="0"
只有当表单里混入了自定义交互元素(比如用 <div role="button"> 实现的“添加一行”按钮),才需显式加 <code>tabindex="0"。但它不是孤立存在的:
- 必须同步加
role="button"或对应语义角色,否则屏幕阅读器读作“div”而非“按钮” - 必须监听
keydown,对Enter和Space触发相同操作(注意Space要e.preventDefault()) - CSS 中若移除了默认
outline,得补:focus-visible { outline: 2px solid #007bff; },否则键盘用户看不见焦点 - 别给
<label></label>加tabindex="0"——它本身不可聚焦,应绑定到关联的<input>上
移动端和 Safari 的 tabindex 行为差异必须手动兜底
iOS Safari 默认只允许 <input>、<textarea></textarea>、<select></select>、<button></button> 和 contenteditable 元素参与 Tab 导航,哪怕你写了 tabindex="0",其他元素也无效。
- 检查系统设置是否开启“辅助功能 → 键盘 → 全键盘控制”,否则 Safari 直接忽略所有
tabindex - 对长列表或滚动区域,用
tabindex="0"+scrollIntoView({ block: 'nearest' })替代纯tabindex控制 - 不要指望
tabindex在移动端实现完整导航逻辑——方向键(ArrowLeft/ArrowRight)行为完全由 JS 拦截管理,tabindex对它无影响 -
focus({ preventScroll: true })在 Safari 中不支持,传对象参数会静默失败;真实项目中应先检测'preventScroll' in FocusOptions.prototype
最易被忽略的一点:焦点移入后元素若被 display: none 或 visibility: hidden 隐藏,.focus() 会失败,哪怕它有 tabindex="-1"。动态组件里,务必确认目标元素已挂载、可见、且未被 CSS 遮挡。











