多级菜单必须用嵌套<ul>和<li>实现语义化结构,子菜单作为父<li>的直接子元素;配合CSS定位与:focus-within,再用JS处理键盘交互、aria-expanded同步及焦点管理,缺一不可。

用嵌套 <ul> 和 <li> 实现语义化多级结构
HTML 本身不提供“菜单组件”,多级菜单本质是嵌套的列表结构。必须用 <ul> 包裹 <li>,子菜单作为当前 <li> 的最后一个子元素——不能丢到外面,否则语义断裂、键盘导航和屏幕阅读器都会失效。
常见错误:把子菜单写成独立的 <div class="submenu"> 放在页面底部,再靠 CSS 定位过去。这会导致 tab 键跳过子项、aria-expanded 失效、移动端点击穿透。
正确骨架示例如下:
<nav aria-label="主菜单">
<ul>
<li><a href="/home">首页</a></li>
<li>
<a href="/products" aria-haspopup="true" aria-expanded="false">产品</a>
<ul class="submenu">
<li><a href="/products/web">Web 端</a></li>
<li><a href="/products/mobile">移动端</a></li>
</ul>
</li>
</ul>
</nav>
CSS 控制显示/隐藏与层级定位
纯 CSS 实现展开收起,关键在父 <li> 的 hover/focus 状态联动子 <ul> 的 display 或 visibility。不要用 opacity: 0 单独隐藏——它仍占据布局空间且无法禁用交互。
立即学习“前端免费学习笔记(深入)”;
要点:
文章转信息图。将文章/笔记转化为手机可读的 HTML 信息图,自动匹配视觉风格。触发场景:文章转图、笔记转图、信息图、转小红书图、做张图、可视化这篇文章、文生图。
- 子菜单默认设为
position: absolute,并用top: 100%对齐父项底部 - 父
<li>必须设position: relative,否则绝对定位会相对于 body 偏移 - 用
:hover > .submenu, :focus-within > .submenu同时支持鼠标悬停和键盘聚焦(focus-within是关键) - 移动端需额外加
click切换逻辑,CSS 无法可靠响应 touch
JavaScript 补足可访问性与移动端交互
仅靠 CSS 的 hover 在触屏设备上完全失效,且键盘用户无法用空格/回车展开菜单。必须用 JS 监听 click 和 keydown 事件,并同步更新 aria-expanded 和 aria-hidden。
核心逻辑:
- 给带子菜单的
<a>绑定click,阻止默认跳转,切换aria-expanded值 - 监听
Enter和Space键,行为与 click 一致 - 子菜单显示时,用
focus()把焦点移到第一个可聚焦子项(如首个<a>) - 按
Escape关闭当前展开的子菜单并返回触发链接 - 避免对所有
<li>监听事件——只处理有aria-haspopup="true"的项
为什么不能直接用 <details><summary>?
<details> 语义是“可折叠内容区块”,不是导航菜单。浏览器对其 role 解析不统一:部分读屏器不识别其为菜单,aria-activedescendant 不生效,且无法原生支持方向键导航(↑↓→←切换子项)。更严重的是,<details> 内部不能放 <a> 作为 summary 文本(会触发两次点击),破坏导航意图。
若强行用,会出现:键盘用户 tab 到 summary 后无法用方向键进入子项、移动端点一次展开、再点一次才跳转链接、SEO 友好性下降(搜索引擎可能忽略 details 内容)。
真正省事的方案不存在。多级菜单的健壮实现,永远需要 HTML 结构 + CSS 定位 + JS 交互 + ARIA 属性四者对齐。漏掉任意一环,都会在某个用户群体中彻底失效。

















