多级折叠菜单必须用嵌套<ul><li>构建语义化结构,禁用<div>模拟;需用>子选择器和:has()精准控制层级显示,事件委托绑定click,避免<details>嵌套。

用 <ul> 嵌套构建语义化层级结构
多级折叠菜单的底层骨架必须是嵌套的 <ul> 和 <li>,不能靠 <div> 堆叠模拟。浏览器和辅助技术依赖这种结构识别父子关系,否则键盘导航、屏幕阅读器都会失效。
关键点:
-
<li>必须直接包裹文本或可点击元素(如<span class="toggle">),子<ul>紧跟其后,顺序不能错 - 每层子菜单都应加
class="nested"或class="submenu",方便 CSS/JS 精准定位,避免后代选择器误匹配 - 不要给顶层
<ul>设display: flex—— 会破坏列表语义,导致缩进、tab 键跳转异常
用 :has(ul) + > 子选择器控制展开状态
CSS 层级控制最常踩的坑是写 .active ul { display: block; },结果一点开父级,所有深层子菜单全弹出来。真正隔离层级,得靠 > 和 :has() 组合。
正确写法示例:
立即学习“前端免费学习笔记(深入)”;
.menu-item > .toggle::after {
content: "▶";
}
.menu-item:has(> .nested) > .toggle::after {
content: "▼";
}
.menu-item.active > .nested {
display: block;
}
说明:
-
:has(> .nested)只匹配「直接子级含 .nested」的<li>,不穿透;IE 不支持,但可配合class="has-child"降级 -
>确保.active > .nested只影响当前<li>下一级,不会波及孙子级 - 别用
visibility: hidden配合动画——它仍占布局空间,会导致点击区域偏移
用事件委托绑定 click,避免重复监听
如果为每个 <li class="menu-item"> 单独绑 addEventListener,DOM 动态增删时极易漏监听或重复绑定。统一挂载到父容器上,靠 event.target 判断是否命中 toggle 元素更稳。
实操要点:
- 监听父
<ul class="menu">的click,而非每个<li> - 用
event.target.closest(".menu-item")获取点击项,再检查它是否含.toggle或是否:has(> .nested) - 切换类名时,只操作当前
<li>的.active,不 touch 兄弟或祖先节点 - 移动端需额外处理
touchstart并preventDefault(),否则 iOS 会触发两次 click
避免用 <details> 实现多级菜单
<details> 是为单层折叠设计的,原生不支持嵌套展开逻辑。强行在 <summary> 里放另一个 <details>,会出现状态不同步、open 属性无法独立控制、键盘焦点丢失等问题。
典型症状:
- 点开二级
<details>,一级自动收起 - 按 Tab 键跳过中间某级,焦点卡死
-
details[open] > summary::before旋转图标在 Safari 中渲染错乱 - 无法用 JS 批量收起所有层级(
querySelectorAll("details").forEach(d => d.open = false)会失败)
真要语义化又省 JS,只能用于单层 FAQ 类场景;多级菜单请老实用 <ul> + 类名控制。


















