必须用nav包裹语义化嵌套的ul/li结构,子ul须为父li直接子元素,缩进仅用padding-left逐级累加,悬停用li:hover>ul并兼顾focus-within或JS切换ARIA状态。

用 ul 和 li 实现有层次感的导航菜单,不是“套几层标签就行”,而是必须满足语义、可访问性与 CSS 选择器链三重约束。绕开嵌套规则,哪怕样式看起来一模一样,也会在键盘导航、屏幕阅读器播报、SEO 索引时出问题。
子菜单必须是父 li 的直接子 ul
这是整个结构能工作的物理前提。CSS 的 :hover 或 :checked ~ 选择器只认“紧邻兄弟”或“直接子元素”,中间插个 div、span 或错位缩进,就会断链。
- ✅ 正确:
<li><a>产品</a><ul class="submenu"><li><a>Web 版</a></li></ul></li> - ❌ 错误:
<li><a>产品</a></li><ul class="submenu">...</ul>(ul跑到li外面) - ❌ 错误:
<li><div><a>产品</a></div><ul>...</ul></li>(ul不再是li的直接子) - ⚠️ 注意:子
ul不能设position: absolute后再给父li加position: relative——这会让焦点框偏移、ARIA 树状关系断裂
nav 必须包裹最外层 ul,不能省
裸 ul 在 HTML 解析时会被自动补全,但语义完全丢失。nav 是唯一被所有主流读屏软件一致识别为“导航区域”的容器,没有它,aria-label="主导航" 就无处挂载,搜索引擎也难区分这是菜单还是普通列表项。
- ✅ 正确:
<nav aria-label="主导航"><ul>...</ul></nav> - ❌ 错误:
<ul class="menu">...</ul>(无nav,语义残缺) - ❌ 避免:
<menu>标签——Chrome 当作ul渲染,Firefox 可能忽略,W3C 明确不鼓励用于导航
缩进只能用 padding-left,别碰 margin 或 text-indent
缩进在这里不是视觉装饰,而是向辅助技术传达层级深度的信号。用错属性会导致 Tab 键顺序错乱、焦点框错位、屏幕阅读器播报层级混乱。
立即学习“前端免费学习笔记(深入)”;
- ✅ 正确:二级菜单
li设padding-left: 20px,三级再加20px,逐级累加 - ❌ 错误:
margin-left—— 把整个li推离文档流,Tab 顺序跳变 - ❌ 错误:
text-indent—— 只缩第一行文字,图标、复选框、箭头位置不受控 - ❌ 危险:
transform: translateX()——:focus-visible框会偏移,无障碍 API 返回的位置信息失真
悬停展开必须用 li:hover > ul,且需兼顾移动端
纯 CSS 悬停方案在桌面端简洁可靠,但在触摸设备上无效。不能只写 li:hover > ul 就完事,得补上 :focus-within 或 JS 切换类,并同步更新 aria-expanded。
- 桌面端基础:
li:hover > ul { opacity: 1; visibility: visible; }(配合transition做平滑) - 移动端 fallback:
li:focus-within > ul(需确保a可聚焦) - JS 方案要点:点击时 toggle
aria-expanded="true/false",同时控制aria-hidden和显隐状态 - ⚠️ 注意:不要用
display: none初始隐藏子菜单——这会让初始 HTML 不可访问;应配合visibility: hidden+opacity: 0+pointer-events: none
真正卡住人的从来不是怎么写几层 ul,而是哪一层该加 aria-expanded、哪个 ul 必须紧跟在 li 里、为什么 padding-left 不能换成 margin。这些细节不处理,菜单看起来正常,用起来却处处断链。



















