子菜单必须是父li的直接子ul,nav必须包裹最外层ul,缩进必须用padding-left,悬停展开需配focus-within或JS并同步aria-expanded。

子菜单必须是父 li 的直接子 ul
不是“建议”,是 DOM 层级硬约束。浏览器、屏幕阅读器、键盘导航(Tab/Arrow 键)和 CSS 选择器(如 li:hover > ul)全部依赖这个紧邻父子关系。中间插一个 div、span,甚至换行符(被解析为文本节点),都会导致结构断裂。
常见错误现象包括:Tab 键跳过整个子菜单、aria-expanded 绑定失效、JS 中 li.querySelector('ul') 返回 null。
- ✅ 正确写法:
<li class="has-submenu">产品<ul class="submenu"><li>Web 版</li></ul></li> - ❌ 错误写法:
<li>产品</li><ul class="submenu">...</ul>(ul脱离li) - ❌ 错误写法:
<li><div>产品</div><ul>...</ul></li>(ul不再是li的直接子)
nav 必须包裹最外层 ul,不能省略
裸 ul 会被浏览器自动补全,但语义完全丢失:读屏软件不识别为导航区域,搜索引擎难区分菜单和普通列表,aria-label 也无处挂载。
用 <menu> 更危险——Chrome 当作 ul 渲染,Firefox 可能忽略,W3C 明确不鼓励用于导航。
立即学习“前端免费学习笔记(深入)”;
- ✅ 正确:
<nav aria-label="主导航"><ul>...</ul></nav> - ❌ 错误:
<ul class="menu">...</ul>(无nav,语义残缺) - ❌ 错误:
<div class="nav"><ul>...</ul></div>(div无导航语义)
缩进必须用 padding-left,逐级累加
缩进在这里不是为了“看起来整齐”,而是向辅助技术传达层级深度。用错属性会导致 Tab 焦点跳变、焦点框偏移、屏幕阅读器播报混乱。
- ✅ 推荐值:
li一级:padding-left: 1rem;二级:padding-left: 2.5rem;三级:padding-left: 4rem - ❌
margin-left:把整个li推离文档流,Tab 焦点跳变 - ❌
text-indent:只缩第一行文字,图标、复选框位置失控 - ❌
transform: translateX()::focus-visible框错位,无障碍 API 返回位置失真
悬停展开必须配 focus-within 或 JS,且同步 aria-expanded
纯 li:hover > ul 在触摸设备上无效,刷新后状态丢失,键盘用户也无法操作。仅靠 CSS 无法持久记忆展开态。
- 桌面端基础:
li:hover > ul { opacity: 1; visibility: visible; }(配合transition) - 移动端/键盘必需:
li:focus-within > ul或 JS 切换aria-expanded="true/false"和类名 - 别忘了给触发元素(如
button或带tabindex="0"的a)确保可聚焦 - 嵌套超过 3 层时,
<details>比ul/li更合适——它原生支持键盘、焦点管理、无需 JS,但不响应hover,也不能批量控制状态



















