仅用 HTML 的 <ol> 和 <ul> 无法实现真正可用的树状菜单,因二者无交互、无状态管理、无可访问性支持;必须配合 CSS 样式控制与 JS 点击逻辑才能实现可折叠、可访问、可动画的树形菜单。

纯靠 HTML 的 <ol> 和 <ul> 嵌套,做不出真正可用的树状菜单——它们只负责结构,不带交互、不控制显隐、没有状态管理。想让菜单可折叠,必须加 CSS 控制样式 + JS 处理点击逻辑。
为什么不能只用 <ol> 套 <ul> 就完事
HTML 列表标签本身没有“展开/收起”语义。嵌套后浏览器只是渲染出缩进层级,所有子项默认可见,用户点不了、关不掉、也看不出哪层是父节点。常见错误是写了一堆嵌套却忘了加交互钩子,结果页面看起来像大纲文档,不是菜单。
更隐蔽的问题是:混用 <ol> 和 <ul> 容易破坏语义一致性。比如用 <ol> 表示主导航(强调顺序),子级却用 <ul>(无序),屏幕阅读器可能误读层级关系;若全用 <ol>,又会强制编号,干扰视觉设计。
- 浏览器不会自动为
<ol>或<ul>添加折叠图标、hover 效果或过渡动画 - 没有
aria-expanded、role="treeitem"等可访问性属性,残障用户无法感知展开状态 - 服务端直出时所有子菜单都可见,无法初始收起指定层级
<ol> 和 <ul> 在树形菜单里怎么分工才合理
如果真要用有序列表,建议仅在「有明确顺序依赖」的场景下作为最外层容器,比如“政策执行步骤”“安装流程指南”,内部统一用 <ul> 做子级嵌套——因为 <ul> 语义更中性,CSS 重置成本更低,JS 操作也更稳定(<ol> 的 start 属性和计数器容易干扰 DOM 查询)。
立即学习“前端免费学习笔记(深入)”;
关键实操点:
- 所有可折叠节点统一用
<li>包裹,内含触发按钮(如<button type="button">)+ 文本 + 子<ul>,避免把<ol>直接塞进<li>再套<ul>,否则 JS 查找nextElementSibling时容易错位 - 给父
<li>加data-toggle="tree-node",而不是依赖标签类型判断是否可折叠——<ol>和<ul>都应被同等对待 - 子菜单一律用
<ul class="tree-children">,不用<ol class="tree-children">,避免 CSS 中要额外重置counter-reset
JS 怎么安全地操作混合列表的展开逻辑
核心是避免全局查询、精准定位当前节点下的子菜单。很多人写 document.querySelectorAll('li > ul'),结果一点击就展开所有同级子菜单,根本不管用户点的是哪一项。
正确做法是用事件委托 + closest() 锚定触发源,再用 nextElementSibling 找紧邻的子菜单:
document.addEventListener('click', e => {
const btn = e.target.closest('[data-toggle="tree-node"] button');
if (!btn) return;
const menu = btn.nextElementSibling;
if (!menu || menu.tagName !== 'UL') return;
const isOpen = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', !isOpen);
menu.classList.toggle('is-open');
});
注意三点:
- 不要用
querySelectorAll('ol li > ul, ul li > ul')这类宽泛选择器,它会跨层级匹配,导致兄弟节点的子菜单也被误操作 - 检查
menu.tagName必须是'UL',哪怕你用了<ol>作外层,子菜单也应强制为<ul>,否则逻辑断裂 -
aria-expanded必须同步更新,否则键盘用户按空格键无法触发,也不符合 WCAG 2.1 标准
CSS 动画别碰 display,优先用 max-height
用 display: none/block 切换会导致子菜单瞬间闪现,没法加过渡效果。而 max-height 可以配合 transition 实现平滑伸缩——但前提是得预设一个足够大的值(比如 max-height: 500px),不然内容多时会被截断。
更稳妥的做法是 JS 动态计算高度:
if (!isOpen) {
menu.style.maxHeight = menu.scrollHeight + 'px';
} else {
menu.style.maxHeight = '0';
}
但要注意:这个方案在子菜单含图片或异步加载内容时可能失效,需监听 load 或用 ResizeObserver 补偿。
最后提醒:如果你只是做个静态文档目录,且只支持现代浏览器,<details><summary> 确实能省事;但只要涉及多级联动、自定义图标、初始展开某一层、或需要兼容 Safari 15 以下,就得老实用 <ul> 结构 + JS 控制——<ol> 在这里只是个可选的语义装饰,不是功能必需品。



















