自定义下拉菜单无法用tab进入,主因是元素不可聚焦;需为触发按钮设tabindex="0"、下拉容器设tabindex="-1"、选项设role="option"并同步aria-selected;必须监听arrow/enter/esc等键盘事件并配合role="combobox"、aria-expanded等aria属性确保可访问性。

focus() 和 tabindex 配合失效的常见原因
自定义下拉菜单无法用 Tab 进入,通常不是 focus() 没调用,而是元素本身不可聚焦。原生 <select></select> 默认可键盘访问,但用 <div> + <code><ul></ul> 实现时,必须显式赋予聚焦能力。
关键点:只有“可聚焦元素”才能被 Tab 切入,而默认情况下 <div>、<code><span></span>、<ul></ul> 都不可聚焦。解决方式是加 tabindex="0"(使其参与顺序聚焦流)或 tabindex="-1"(仅支持脚本聚焦,比如关闭菜单后把焦点还给触发按钮)。
- 触发按钮(如
<button></button>)必须存在且为原生<button></button>,不能用<div role="button"> 代替——后者需手动处理 <code>Enter/Space事件 - 下拉容器(
<ul></ul>或<div role="list">)应设 <code>tabindex="-1",避免 Tab 穿透进去;真正可操作的选项用<li role="option">+tabindex="-1" - 不要给整个菜单容器设
tabindex="0",否则用户 Tab 一次就跳进菜单,再 Tab 一次又跳出去,中间无法操作选项 -
ArrowDown和ArrowUp:在打开状态下,循环聚焦到下一个/上一个role="option"元素,用element.focus()主动切换 -
Enter和Space:触发当前聚焦项的选中动作(如设置值、更新隐藏<input type="hidden">),然后收起菜单 -
Escape:立即收起菜单,并把焦点还原到触发按钮(用triggerButton.focus()) - 不要监听
click来替代Enter/Space——键盘用户不会触发 click,且 screen reader(如 NVDA)对按钮的Space行为有固定预期 - 触发按钮需同时具备:
role="combobox"、aria-haspopup="listbox"、aria-expanded="false"、aria-controls="id-of-dropdown" - 下拉容器需为:
role="listbox",且 ID 与aria-controls值一致 - 每个选项必须是:
role="option",并带aria-selected="true/false"(选中状态要实时同步) - 遗漏
aria-expanded是最常见疏漏——它直接导致屏幕阅读器不播报菜单开闭状态 - 优先用
focusout检测焦点是否移出整个菜单组件(包括触发按钮 + 下拉区域),而不是依赖click - 在
focusout回调里,用event.relatedTarget判断焦点去向;如果relatedTarget是null(如失焦到地址栏)或不在菜单 DOM 树内,才关闭并还原触发按钮焦点 - 务必加
setTimeout延迟执行关闭逻辑,因为 Firefox 中focusout触发时relatedTarget可能还未更新 - 移动端 Safari 对
focusout支持不稳定,此时需 fallback 到blur+touchstart组合判断
键盘事件监听必须覆盖 Enter/Space/ArrowDown/Escape
仅靠 tabindex 不足以完成键盘导航。用户按下方向键要移动焦点,回车/空格要选中,ESC 要收起菜单——这些都得手写事件逻辑,且不能只监听 keydown,要兼顾语义和屏幕阅读器行为。
role="combobox" 和 aria-expanded/aria-controls 的必要性
光有功能还不够,屏幕阅读器需要明确的语义提示。把触发按钮标记为 role="combobox",并关联下拉区域,是让 JAWS/NVDA 正确播报“下拉列表,已折叠/已展开”的唯一可靠方式。
focusout 与点击外部关闭的冲突处理
很多实现用 document.addEventListener('click', closeIfOutside) 关闭菜单,但这会和键盘用户的 Tab 移出行为冲突:当焦点从最后一个选项移到页面其他地方时,click 事件还没触发,focusout 却先发生了,结果菜单闪退,焦点丢失。
真正的难点不在代码量,而在于所有交互路径(Tab/Shift+Tab/Arrow/Enter/ESC/鼠标点击/触摸)必须收敛到同一套状态机,且每一步都要同步 ARIA 属性、DOM 焦点、视觉样式和隐藏字段值。少同步一项,键盘或屏幕阅读器用户就会卡住。











