用 + 可零 js 实现语义化折叠树,适合静态场景;多级联动、搜索过滤等需 js 控制,推荐扁平数组+id/parent_id 管理数据结构,配合 aria 属性与 css 类切换状态。

直接用 <ul></ul> + <li> 嵌套就能实现基础商品分类树,不需要 JS 也能展开收起(靠 <details></details>),但真要支持多级联动、搜索过滤、选中状态同步,必须加 JS 控制逻辑。
用 <details></details> + <summary></summary> 快速实现无 JS 折叠树
适合静态分类、后台生成 HTML 的场景,语义清晰、无障碍友好、零 JS 依赖。浏览器兼容性从 Chrome 12 / Firefox 49 / Safari 6 开始都支持,IE 完全不支持。
注意 <details></details> 默认是闭合的,加 open 属性可默认展开;<summary></summary> 只能有一个子元素(否则部分浏览器渲染异常);嵌套时子 <details></details> 不会自动继承父级状态。
-
手机
iPhone
- iPhone 15
- 安卓
- 不要在
<summary></summary>里放<button></button>或其他交互控件,会破坏原生行为 - CSS 控制箭头图标需用
details[open] > summary::after,不能只靠::marker(Firefox 不支持) - 移动端点击区域小,建议给
<summary></summary>加padding并设cursor: pointer
用 data-id + data-parent 构建可操作的分类数据结构
前端维护树形菜单时,光靠 DOM 嵌套很难做动态增删、异步加载或跨级联动。推荐用扁平数组存分类,靠 id 和 parent_id 关系还原层级 —— 这是后续所有 JS 操作的基础。
后端返回的 JSON 示例:
[{"id":1,"name":"手机","parent_id":0},{"id":2,"name":"iPhone","parent_id":1},{"id":3,"name":"iPhone 15","parent_id":2}]
- 前端递归生成 DOM 时,避免直接拼接字符串,用
document.createElement或模板字符串 +innerHTML都行,但要注意 XSS(服务端已转义则安全) - 每个
<li>必须带data-id="2",对应节点 ID;父级<ul></ul>最好也带data-level="2",方便 CSS 区分缩进层级 - 不要用
class="level-2"这类硬编码样式名,层级深了难维护;改用margin-left: calc(var(--indent) * 2)
JS 控制展开/选中状态时,别直接操作 style.display
手动设 display: none/block 会丢失 CSS 过渡动画、破坏可访问性(ARIA 属性不同步)、且无法响应窗口 resize 后的重排。
正确做法:用 aria-expanded + aria-hidden 控制语义,用 CSS 类切换控制显隐(配合 max-height 或 transform 实现过渡)。
- 点击分类项时,先查该节点所有子节点的 DOM 元素,统一加
is-expanded类,再设aria-expanded="true" - 选中单选模式(radio)还是多选模式(checkbox),取决于业务:商品筛选页常用多选,后台类目管理常用单选
- 如果支持“点击文字展开、点击复选框选中”,两个事件必须解耦,否则
label包裹input会导致点击 summary 时 checkbox 也被触发
移动端折叠菜单容易忽略的三个细节
用户手指操作精度低、屏幕空间小、手势干扰多,光靠桌面端逻辑会出问题。
- 禁用
touchstart时的默认滚动行为(event.preventDefault())要谨慎:只在真正需要阻止滚动的区域加,否则页面整体滑动卡顿 - iOS Safari 下
<details></details>的open状态可能在页面回退后丢失,需监听pageshow事件恢复 - 层级超过 3 级时,别让用户一路点回去,加个“返回上一级”按钮(固定在顶部或左上角),
data-back-to="1"存上级 ID 即可
最麻烦的不是怎么画出树,而是分类数据变更后如何最小化更新 DOM —— 如果用原生 JS,建议把节点映射成 MapquerySelectorAll 查找快得多。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











