最简树形菜单应使用 ul+li 嵌套实现,子菜单作为下级 ul 放在对应 li 内;父级 li 包含触发 button 和子 ul,通过 class 控制显隐并同步 aria-expanded 属性,确保无障碍与 SEO 友好。
ul + li 嵌套实现最简树形菜单树形菜单本质是层级结构,HTML 原生不提供 tree 标签,所以必须靠语义化嵌套。最稳妥、兼容性最好、SEO 友好的方式就是用 ul 套 li,子菜单作为下级 ul 放在对应 li 里。
常见错误是把子菜单写成同级元素、或用 div 模拟层级——这样既无法键盘导航,也不被屏幕阅读器识别为可折叠结构。
li 应包含一个触发按钮(如 button)和一个子 ul
ul 默认加 display: none,点击按钮再切换 display 或 classaria-expanded 和 aria-controls 属性,否则无障碍支持失效纯 CSS 实现折叠需要借助 :checked + 隐藏的 input[type="checkbox"],但限制大(无法 JS 动态控制、不支持多级联动)。更实用的是用 class 控制显隐,配合过渡动画。
关键点不是“怎么动”,而是“怎么保持结构清晰”:
立即学习“前端免费学习笔记(深入)”;
margin-left 或 padding-left 表示层级,别用绝对定位压位置::before 实现,避免冗余 HTML 标签ul 加 max-height + overflow: hidden 才能做高度过渡,仅靠 opacity 不够直观.menu-item > ul { max-height: 0; overflow: hidden; transition: max-height 0.2s ease-out;}.menu-item.is-open > ul { max-height: 500px; /* 足够容纳所有子项 */}
核心逻辑就三步:找按钮 → 切换目标 ul 的显示状态 → 同步更新 aria-expanded。别用 toggle() 这种封装过深的方法,容易掩盖 DOM 状态不一致问题。
.menu 容器,比给每个按钮单独绑定更轻量closest() 定位当前 li,再用 querySelector() 找子 ul,避免硬编码 class 名button.setAttribute('aria-expanded', 'true'),不要只靠 class 判断aria-expanded="false"
details/summary?看起来 details 是原生树形组件,但它有硬伤:不支持多级嵌套(子 details 在部分 Safari 版本中无法响应)、无法自定义箭头方向、open 属性无法通过 JS 可靠读取(尤其 SSR 渲染后)。实际项目中,一旦菜单超过两级或需统一交互风格,就得退回到 ul + JS 方案。
还有一点容易被忽略:服务端渲染(SSR)页面首次加载时,若用 details,浏览器会先展开再闪一下收起——因为 JS 还没执行,而 ul 方案可以靠初始 class 精确控制首屏状态。