tabindex在下拉组件中决定焦点流向:触发按钮原生元素无需设tabindex="0",自定义触发器需设;面板容器和选项均应设tabindex="-1"以支持js聚焦与方向键导航,禁用正整数避免tab流错乱。

tabindex 在下拉列表中不是用来“排序”或“显隐”的开关,而是决定谁该进 Tab 流、谁该被脚本聚焦的关键开关。用错值(尤其是正整数)会直接让键盘用户在菜单里迷路。
哪些元素该加 tabindex="0",哪些绝对不该加
下拉触发按钮(如 <button></button> 或带 role="combobox" 的 <div>)必须可聚焦,但方式不同:
<ul>
<li>原生 <code><button></button>、<input type="text"> 默认就等价于 tabindex="0",**加了反而多余**,可能干扰 React hydration 或 Safari 的 focus 管理
<div role="combobox">)必须加 <code>tabindex="0",否则键盘用户根本 Tab 不进去
<ul class="dropdown-menu"></ul>)**绝不能加 tabindex="0"**——它不该是 Tab 流中的一个停靠点;正确做法是设 tabindex="-1",仅用于 JS 主动聚焦(比如展开后跳到第一个选项)<li role="option">)一律设 tabindex="-1",配合 aria-activedescendant 实现单次 Tab 进入 + 方向键导航
tabindex="-1" 在下拉展开/收起时的实际作用
它不是“隐藏”,而是“临时接管”。下拉面板默认不可见,但若没 tabindex="-1",JS 调用 .focus() 会静默失败——哪怕你写了 panel.focus(),浏览器也直接忽略。
- 面板展开后,立即对第一个选项调用
options[0].focus(),前提是它有tabindex="-1" - 用户按
Escape关闭时,焦点应回退到触发按钮;若按钮是原生<button></button>,不用设tabindex,直接triggerBtn.focus()即可 - 移动端要注意:iOS Safari 中
focus({preventScroll: true})不支持,得降级为focus()后手动scrollIntoView({block: 'nearest'}) - 别在
display: none的元素上调用.focus()——它不会报错,但也不会生效
为什么 tabindex="1" 是下拉组件中最危险的写法
它不会让你的下拉按钮“第一个被 Tab 到”,只会把它踢出 DOM 顺序,塞进所有正数里按数值排,然后统一压在所有原生控件(<input>、<button></button>)之后——结果是:Tab 到最后一个表单项,突然跳回顶部的下拉按钮,再跳回中间某个输入框,路径完全断裂。
- 多个
tabindex="1"共存时,浏览器只认 DOM 中第一个,其余失效 - SSR 渲染时服务端和客户端 tabindex 值不一致,React 会抛 hydration warning
- 屏幕阅读器依赖 DOM 顺序理解上下文,强行插队会让视障用户无法建立操作预期
- 真正需要控制流转时机的场景(比如城市输入后自动跳到下拉帮助),应该用
tabindex="-1"+keydown拦截 + 显式.focus(),而不是靠tabindex="1"硬塞
最易被忽略的一点:下拉面板即使设了 tabindex="-1",若没同步加 aria-hidden="true"(关闭时)或 aria-hidden="false"(展开时),屏幕阅读器仍可能朗读不可见内容。这个状态切换必须和显隐逻辑严格绑定,不能只靠 CSS。











