ul+li嵌套是构建可访问树形菜单的标准方式,需配合role属性、aria-expanded/hidden同步控制、max-height或transform动画及键盘导航支持。

用 ul + li 嵌套是最直接、语义正确的方式
HTML 本身没有原生树形组件,但 ul 和 li 天然支持多层嵌套,是构建可访问、可样式化、可键盘导航的树形菜单的事实标准。别想着用 div 堆砌或 JS 动态生成结构——那会让屏幕阅读器困惑,也增加 CSS 维护成本。
关键点:
- 每级子菜单必须是上一级
li的直接子元素ul,不能放在div或其他容器里 - 父级
li应包含触发控件(如带aria-expanded的button),不能只靠点击文字展开 - 所有可交互节点需有明确角色:根级用
role="tree",节点用role="treeitem",容器用role="group"
aria-expanded 和 aria-hidden 必须同步控制显隐与可访问性
仅用 CSS 的 display: none 或 visibility: hidden 隐藏子菜单,会导致屏幕阅读器仍能读取内容,且焦点可能落入不可见区域。必须配合 ARIA 状态管理。
常见错误现象:
立即学习“前端免费学习笔记(深入)”;
- 菜单展开后,按 Tab 键焦点跳进已隐藏的子项,用户卡住
- 用 JS 切换
display却忘了更新aria-expanded="false",辅助技术误判状态 - 子
ul没加aria-hidden="true",即使视觉隐藏,NVDA 仍朗读全部子项
正确做法:每次切换时,同时操作两个属性:
button.setAttribute('aria-expanded', 'true');<br>submenu.setAttribute('aria-hidden', 'false');
CSS 展开/收起动画不能依赖 height: 0 → auto
想加淡入或滑动效果很自然,但直接对 ul 设置 transition: height .2s 并设 height: 0 会失败——因为 height: auto 无法参与 CSS 过渡。浏览器不计算自动高度的中间值。
可行方案(选其一):
- 改用
max-height过渡(例如从max-height: 0→max-height: 500px),需预估最大行数对应像素值 - 用
transform: scaleY(0)→scaleY(1),配合transform-origin: top模拟下拉,性能更好且无需预估尺寸 - 放弃高度动画,改用
opacity+visibility实现淡入,更稳妥
键盘导航必须实现 ArrowUp/ArrowDown 和 Enter/Space
仅鼠标点击能展开不够。WCAG 要求树形菜单支持方向键遍历:上下键在同级移动,左右键收起/展开,Enter/Space 触发默认行为(如跳转或展开)。否则残障用户无法使用。
实操要点:
- 监听
keydown事件,阻止默认行为(如页面滚动) - 维护当前聚焦的
treeitem元素引用,避免每次遍历 DOM -
ArrowRight只对已展开的父项生效(避免空展开),ArrowLeft对子项生效时先收起,再跳到父项 - 所有可聚焦节点必须有
tabindex="-1"(非初始可 tab,由 JS 控制焦点流)
忽略这点,树菜单在键盘模式下基本等于不可用——哪怕视觉效果再炫。



















