不能只靠 <details> 实现生产级树导航,因其存在 Safari 嵌套状态不稳定、无法初始展开指定层级、样式与交互受限、不支持键盘操作、无障碍不达标、事件机制缺失等根本缺陷。

直接用 <details> + <summary> 就能实现基础层级折叠,但仅限静态、浅层、不需联动的场景;真要支持多级联动、状态保持、键盘操作和样式定制,必须用 <ul>/<li> 结构配合 JavaScript 控制 aria-expanded 和 max-height 动画。
为什么不能只靠 <details> 实现生产级树导航
<details> 看似省事,实际在树形导航中很快暴露短板:
- Safari 对嵌套
<details>的open状态支持不稳定,子节点常被父节点 toggle 重置 - 无法初始展开指定层级(服务端渲染后所有节点默认闭合)
-
<summary>的点击热区和箭头图标无法通过 CSS 完全重置,加图标或 hover 效果会破坏原生行为 - 不支持键盘空格/回车切换,
aria-expanded需手动同步,无障碍不符合 WCAG 要求 - 没有事件机制区分“点文字”和“点图标”,也无法阻止冒泡,导致内部复选框误触发展开
用 <ul>/<li> 搭建可交互树结构的关键约定
结构不是随便嵌套,得让 JS 和 CSS 能可靠识别和操作:
- 每个可展开节点的
<li>必须带data-expandable="true",叶子节点不加该属性 - 子
<ul>必须紧跟在触发按钮之后、同一<li>内部,例如:<li data-expandable="true"><button aria-expanded="false">目录</button><ul>...</ul></li> - 不要用
display: none控制隐藏——它会让子节点不可访问;改用max-height: 0+overflow: hidden配合 CSS 过渡 - 缩进统一用
padding-left,别依赖嵌套深度计算(如li:nth-of-type(1) > ul),否则动态增删节点时样式错乱
JavaScript 展开/折叠逻辑怎么写才不翻车
核心是事件委托 + 状态同步 + DOM 定位精准,不是给每个 <button> 绑 onclick:
立即学习“前端免费学习笔记(深入)”;
- 在外层容器(如
<ul class="tree">)上监听click,用event.target.closest('li[data-expandable]')捕获目标节点 - 立刻调用
event.stopPropagation(),防止点击内部<input type="checkbox">或链接时触发父节点展开 - 取当前
<button>的aria-expanded值,用classList.toggle('is-open')切换视觉类,再更新aria-expanded为"true"或"false" - 定位子
<ul>用button.nextElementSibling,而不是querySelector('ul')——后者可能匹配到兄弟节点里的<ul> - 状态存在
aria-expanded里,别用布尔值字符串比较(如=== 'true'),直接用getAttribute('aria-expanded') === 'true'
图标、连线、键盘支持这些细节怎么补全
视觉线索和可访问性不是锦上添花,而是树形导航可用的前提:
- 展开/折叠图标用
::before伪元素,配合.is-open button::before { content: "▼"; },避免 innerHTML 替换带来的重排 - 连线用
::after在<li>上绘制垂直线段,配合position: relative和top偏移控制起点 - 键盘支持:监听
keydown,对Space和Enter键执行同 click 逻辑;用tabindex="0"确保<button>可聚焦 - 每个
<li>上挂原始数据引用,如li.__data = node,后续搜索、选中、懒加载都靠它,别反复查 DOM 属性
真正难的不是写出第一级展开,而是当用户连续点击三级节点、同时打开五个分支、又用键盘导航回到第二级再按 Space 时,状态仍准确、动画不卡顿、屏幕阅读器播报正确——这些边界情况决定了你写的到底是“能跑的 demo”,还是“可交付的导航器”。



















