不是必须,但不用嵌套ul就等于主动放弃语义、键盘导航和屏幕阅读器支持;二级ul必须是一级li的直接子元素,三级ul必须是二级li的直接子元素,且需配合role="menuitem"、aria-haspopup="menu"等属性保障可访问性。

必须用嵌套 ul 吗?语义和可访问性决定答案
不是技术上“必须”,但不用嵌套 ul 就等于主动放弃语义、键盘导航和屏幕阅读器支持。用 div 堆出三层视觉效果,ul 里只放一级,二级靠 JS 动态插入——这类结构在 NVDA + Firefox 下大概率读作“列表,1 项”,子菜单直接被跳过;Tab 键也无法进入二级项。
真正起作用的是 DOM 层级关系 + ARIA 属性协同:
-
ul必须是li的直接父容器,不能靠position: absolute把子菜单“飞”出去再塞进div - 每个可展开的
li需设role="menuitem",对应子ul设role="menu"和aria-labelledby -
aria-haspopup的值只能是"menu",写成"true"是 W3C 已废弃的错误用法
:hover 在三级菜单失效的真正原因
不是 CSS 写错了,而是浏览器对 :hover 的定位逻辑和 DOM 流失共同导致的。二级 ul 一旦用了 position: absolute,它就脱离文档流;它的子 li 的 :hover 区域,可能和鼠标实际位置错开几十像素——尤其当父 li 没设 position: relative 时,绝对定位会向上找最近的有定位的祖先,造成偏移。
关键控制点:
立即学习“前端免费学习笔记(深入)”;
- 每一级
li都必须加position: relative - 三级
ul不要用margin-left推开,改用left: 100%+top: 0实现右向展开 - 避免给
ul设display: none后再用:hover切换——这会让 screen reader 完全忽略该节点;改用visibility: hidden+aria-hidden="true"
纯 CSS 多级菜单能跑通的最小结构条件
只要结构不满足这三条,哪怕样式再漂亮,键盘用户也进不去第二层,移动端点一下就收,或者三级菜单悬停中断。这不是兼容性问题,是 DOM 关系断裂。
- 二级
ul必须是**一级li的直接子元素**(不能包在div里) - 三级
ul必须是**二级li的直接子元素**(不能靠 JS 插入或兄弟选择器模拟) - 每个触发展开的元素(如
a或button)必须同时带aria-haspopup="menu"和aria-expanded="false",且初始状态与对应ul的显隐一致
为什么键盘导航必须手写?浏览器不帮你
CSS :hover 只响应鼠标,浏览器不会自动把一堆 a 或 li 当作一个逻辑菜单来处理焦点流。按 Tab 进入一级后,按 → 键毫无反应,按 ↓ 键也不会循环聚焦当前级菜单项——因为 DOM 里没有声明它们属于同一个 role="menu" 上下文。
必须手动监听 keydown 并实现:
- ArrowDown / ArrowUp:在当前
role="menu"内循环聚焦role="menuitem" - Escape:关闭当前展开的子菜单并把焦点移回触发按钮
- Home / End:跳转到第一个或最后一个
menuitem - 禁用项必须设
aria-disabled="true"+tabindex="-1",否则键盘仍可聚焦
最易被忽略的是:所有菜单项的 tabindex 不能全设为 0,否则 Tab 键会跳出菜单去下一个页面元素;正确做法是仅让当前级可见项可聚焦,其余设 tabindex="-1"。



















