子菜单必须是父li的直接子ul,nav必须包裹最外层ul,缩进仅用padding-left逐级累加,悬停需配合focus-within或js并同步aria-expanded。

子菜单必须是父 li 的直接子 ul
这不是样式问题,而是 DOM 结构硬约束。浏览器和屏幕阅读器只认 li > ul 这种紧邻父子关系,中间插个 div、span 或换行符(被解析为文本节点)都会断链。
常见错误现象:Tab 键跳过子项、aria-expanded 绑定失效、JS 用 li.querySelector('ul') 返回 null。
- ✅ 正确写法:
<li>产品<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></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"和类名 - 别忘了给触发元素(如
label或button)加tabindex="0",确保可聚焦
嵌套超过 3 层时,<details></details> 比 ul/li 更合适——它原生支持键盘、焦点管理、无需 JS,但不响应 hover,也不能批量控制状态。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











