原生<select>无法实现多级下拉菜单,必须用嵌套<ul><li>结构;需严格遵循语义化HTML、ARIA属性(aria-haspopup/expanded、role="menu"/"menuitem")及键盘交互逻辑,否则将破坏可访问性与跨端兼容性。

原生 <select> 根本不能做多级下拉菜单,别试了——浏览器会把所有 <optgroup> 里的选项压平显示,不支持嵌套、hover 展开、键盘导航,也不识别子菜单语义。
必须用 <ul><li> 嵌套结构
这是唯一能兼顾语义、可访问性、键盘操作和移动端兼容的方案。子 <ul> 必须作为父 <li> 的直接子元素,否则:
- CSS 选择器(如
li:hover > ul)会失效 - 屏幕阅读器读不出层级关系,可能跳过整个菜单
- Tab 键无法按深度顺序进入子项,焦点流断裂
- 移动端
:focus-within或 JS 切换类逻辑也会出错
正确结构示例:
<nav aria-label="主导航">
<ul>
<li>
<a href="#" aria-haspopup="true" aria-expanded="false">产品</a>
<ul role="menu">
<li role="menuitem"><a href="/a-series">A系列</a></li>
<li role="menuitem"><a href="/b-series">B系列</a></li>
</ul>
</li>
</ul>
</nav>
aria-expanded 和 aria-haspopup 不是可选,是强制要求
没这两个属性,视障用户根本不知道这是可展开菜单,NVDA 或 VoiceOver 会直接忽略它。常见错误是只加 CSS 显隐,却忘了同步 ARIA 状态:
立即学习“前端免费学习笔记(深入)”;
- 一级触发元素(
<a>或<button>)必须带aria-haspopup="true"和初始aria-expanded="false" - 每次点击/Enter/Space 切换子菜单时,JS 必须同步更新
aria-expanded值 - 子
<ul>必须加role="menu",每个子项<li>加role="menuitem" - 不要用
pointer-events: none隐藏子菜单——会导致键盘焦点丢失
别依赖纯 :hover,移动端和键盘用户会卡住
.menu li:hover > ul { display: block; } 在桌面端看似能用,但实际问题一堆:
- iOS 和 Android 上 hover 触发不可靠,尤其在 Safari 中经常不响应
- 键盘用户按 Tab 进入一级项后,无法用方向键打开子菜单
- 触摸设备没有 hover 概念,纯 CSS 方案等于对手机用户关闭功能
- 正确做法:用 click + keydown(Enter/Space)+ focusin/focusout 组合控制显隐
- 最简健壮逻辑:点击任一父项时,先收起其他所有已展开的子菜单,再切换当前项,并更新对应 ARIA 属性
真正麻烦的不是写几行 CSS,而是让菜单在鼠标、触屏、键盘、屏幕阅读器下行为一致——结构定生死,ARIA 是底线,JS 只补交互缺口。漏掉任何一个环节,都可能让一部分用户完全无法使用这个菜单。



















