多级下拉菜单需用嵌套构建语义化结构,每级可展开项设position: relative,子菜单用position: absolute精确定位;隐藏用opacity:0;visibility:hidden;pointer-events:none三属性组合;移动端结合focus-within与轻量JS控制开关。

用嵌套 <ul></ul> + <li> 构建语义化层级结构
多级下拉菜单的底层布局不是靠 div 堆砌,而是靠标准列表嵌套表达父子关系。浏览器和屏幕阅读器只认 <ul></ul> 包含 <li>、<li> 再包 <ul></ul> 这种结构,才能正确识别“Link 2 属于主菜单,且它下面有 Link A 和 Link B”。
常见错误是把子菜单写成兄弟节点(比如 <li>Link 2</li>
<ul class="submenu">...</ul>),这样 CSS 无法精准控制“只展开 Link 2 对应的子菜单”,结果一 hover 就全弹出来。
- 每个可展开项必须是
<li class="has-submenu">,内部包裹文字和子<ul class="submenu"></ul> - 子
<ul></ul>必须是父<li>的直接子元素,不能隔层或平级 - 避免在
<ul></ul>外加<div> 包裹子菜单——这会破坏语义流,让 :hover > ul 选择器失效 <h3> <code>position: relative和position: absolute的定位组合必须配对使用纯靠
display: none/block切换显示,会导致菜单弹出时页面跳动、遮挡其他内容、定位偏移。真正稳定的定位依赖父级设position: relative,子级用position: absolute锚定位置。容易踩的坑是只给最外层
<ul></ul>加relative,而忘了每一级可展开的<li>都要设——否则二级菜单会相对于 body 定位,三级菜单又相对于一级菜单错位,越深偏得越离谱。立即学习“前端免费学习笔记(深入)”;
- 一级菜单项:
<li>设position: relative,为二级菜单提供定位上下文 - 二级菜单
<ul class="submenu"></ul>:设position: absolute; top: 100%; left: 0 - 三级及以上菜单:统一设
left: 0; top: 0,避免left: 100%在多层嵌套中累积偏移
隐藏逻辑别只用
display: none,要用三属性组合用
display: none隐藏菜单,动画过渡会失效,而且切换瞬间会触发重排(reflow),导致闪屏或布局抖动。更稳妥的做法是用opacity: 0; visibility: hidden; pointer-events: none组合。这个组合不是为了炫技,而是解决实际问题:保持 DOM 占位(不触发重排)、支持 CSS transition、阻止误点穿透(比如鼠标滑过隐藏菜单区域却点中了后面的按钮)。
- 必须同时设置三个属性,缺一不可;单设
visibility: hidden仍响应鼠标事件,单设opacity: 0仍可点击 - 显示时对应设为
opacity: 1; visibility: visible; pointer-events: auto - transition 推荐加在
opacity上,比height或transform更稳定,尤其在子菜单高度不固定时
移动端悬停失效?
:hover不是万能的,但 JS 不该是第一选择在 touch 设备上,
:hover行为不可靠:第一次 tap 触发 hover,第二次才真正跳转,体验割裂。但立刻上 JS 监听touchstart又容易误触——手指刚落就展开,滑动页面时菜单乱弹。真正实用的解法是用媒体查询 +
focus-within+ 少量 JS 回退:桌面端走li:hover > ul,移动端默认收起,仅当用户 tap 某个li.has-submenu时,给它加临时 class(如is-open),再用.is-open > .submenu控制显示。这样既避免误触,又保留键盘焦点导航能力。- 不要监听
touchend——延迟高,用户松手才响应,感知卡顿 - 加
tabindex="0"到可展开的<li>,确保键盘用户能用 Tab 进入、Enter 展开 - JS 只负责开关状态,样式控制全部留在 CSS 里,便于维护和动画复用
- 一级菜单项:



















