原生 不支持可靠的多级树形菜单,因其无状态联动、不可监听展开、不兼容键盘导航与 aria;必须用 + + javascript 实现可访问、可控、可维护的树形结构。

details标签本身不支持多级嵌套展开逻辑
直接在 <details></details> 内部再放一个 <details></details>,浏览器会渲染出来,但点击子 <details></details> 时父级不会保持展开、无法联动控制、也不提供任何层级状态反馈——它只是视觉堆叠,不是真正的树形菜单。
真正可用的“多级树形菜单”,需要靠 CSS 控制显示/隐藏 + JavaScript 维护展开状态。原生 <details></details> 的 open 属性是单向的,不能被 JS 可靠监听(toggle 事件不冒泡,且无 openchange 标准事件),所以不适合做多级交互中枢。
- 如果你只想要静态折叠效果(比如文档目录预览),嵌套
<details></details>可用,但用户操作不可预测 - 如果需要点击父项展开子项、再次点击收起、支持键盘导航、焦点管理或 ARIA 标准,必须用
<button></button>+<ul></ul>+ JS - 所有主流 UI 库(如 Headless UI、Reach UI)都避开了
<details></details>做树形控件,原因就在这儿
用 button + ul + aria-expanded 模拟可访问树形菜单
这是目前最稳妥、兼容性好、屏幕阅读器友好的做法。核心是把每个可展开节点换成 <button></button>,用 aria-expanded 表达状态,用 aria-controls 关联子列表,再配合 CSS 控制 max-height 或 display 实现动画。
示例结构:
- 每个
<button></button>必须有唯一id或通过aria-controls明确指向子容器 -
hidden属性比display: none更利于可访问性,但需配合 JS 切换 - 不要依赖
click之外的事件(如keydown中处理 Space/Enter)否则键盘用户无法操作
JS 展开/收起逻辑的关键细节
重点不是“怎么切换 display”,而是“怎么保证状态同步”和“怎么避免事件干扰”。常见翻车点包括:子菜单点击触发父级收起、多次快速点击导致状态错乱、键盘操作后焦点丢失。
- 监听
click和keydown(对Space和Enter做 preventDefault) - 用
element.getAttribute('aria-expanded') === 'true'判断当前状态,别用element.hidden—— 它可能被 CSS 覆盖 - 切换时先改
aria-expanded,再操作 DOM,这样屏幕阅读器能及时播报 - 子菜单展开后,把焦点移到第一个可聚焦子项(如第一个
<a></a>或<button></button>),否则键盘用户会跳到页面底部
为什么不用 CSS-only 方案(比如 :checked + label)
有人尝试用 <input type="checkbox"> 配合 :checked ~ ul 实现纯 CSS 树形菜单。它能在部分场景工作,但有硬伤:
- 无法用键盘操作(
Tab进入后按Space不触发:checked切换) - 移动端 Safari 对
:checked的样式响应有延迟甚至失效 - 无法读取当前展开状态供其他逻辑使用(比如保存用户偏好到 localStorage)
- ARIA 属性无法动态绑定,不符合 WCAG 1.3.1(信息与关系)要求
这些限制在真实产品中都会暴露,尤其当菜单需要记忆展开状态、支持搜索高亮或与路由联动时,CSS-only 就彻底不可行了。
树形菜单的复杂度不在 DOM 结构,而在状态边界和交互反馈。用 <details></details> 看似省事,实则把问题推给了用户和辅助技术。真要落地,button + ul + JS 是绕不开的路径。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











