嵌套 ul 是可访问性强制要求:二级 ul 必须是某一级 li 的直接子元素,三级 ul 同理必须嵌在二级 li 里;错误结构会导致屏幕阅读器读不出子菜单、:hover 失效、键盘导航异常。

必须用嵌套 ul,否则键盘和屏幕阅读器直接失效
不是浏览器限制,而是可访问性规范强制要求:二级 ul 必须是某一级 li 的**直接子元素**,三级 ul 同理必须嵌在二级 li 里。用 div 包一层、或靠 JS 动态插入到 body 底部再用 position: absolute 拉回来——这种结构在 NVDA + Firefox 下大概率被读作“列表,1 项”,子菜单彻底不可见。
常见错误写法:(子菜单和 li 并列),这会让 CSS 的 li:hover > ul 完全不生效,因为子 ul 根本不在 li 内部。
- 每级可展开的
li需加role="menuitem"和aria-haspopup="menu" - 对应子
ul必须设role="menu"和aria-labelledby(指向触发它的链接 ID) -
aria-expanded初始值要和子菜单显隐状态严格一致,不能靠 CSS 控制后才补属性
:hover 在三级菜单失效,90% 是 position 错位
不是 CSS 写错了,而是脱离文档流后,鼠标实际坐标和 :hover 检测区域错开。二级 ul 用了 position: absolute,但它的父 li 没设 position: relative,浏览器就会向上找最近的定位祖先——可能跳到 nav 或 body,导致三级菜单悬停热区偏移几十像素。
- 每一级
li都必须加position: relative - 三级
ul不要用margin-left推开,改用left: 100%+top: 0 - 避免用
display: none控制显隐:screen reader 会直接跳过该节点;改用visibility: hidden+aria-hidden="true"
移动端点击展开必须手写逻辑,:hover 在触屏上根本没用
CSS :hover 在 iOS Safari 和多数安卓浏览器中只在模拟鼠标时短暂触发,点一下就收,无法维持展开态。纯 CSS 方案必须退回到 checkbox + label 模式,否则必须用 JS 监听 click 并手动切换 aria-expanded 和类名。
- 用隐藏
控制对应ul显隐,label 关联触发 - CSS 用
input:checked ~ ul(注意是通用兄弟选择器,ul必须紧跟在 label 后面) - JS 实现时,别用递归绑定
click,容易重复监听;应委托到最外层nav,用e.target.closest("li[role='menuitem']")判断触发源
键盘导航不能靠浏览器自动处理,ArrowDown/ArrowUp 必须自己实现
浏览器不会因为你写了 role="menu" 就自动把子项当菜单处理。Tab 进入一级后,按 ↓ 键默认焦点会跳到下一个可聚焦元素(比如按钮或输入框),而不是当前菜单里的下一项。
必须监听 keydown 并做三件事:
- 捕获
ArrowDown/ArrowUp,在当前role="menu"内循环聚焦所有role="menuitem" - 捕获
Enter或Space,触发展开/收起并同步aria-expanded - 捕获
Escape,关闭当前展开的所有层级,并把焦点移回触发它的菜单项
真正难的不是写逻辑,而是每次展开新层级时,要确保焦点能落到第一个子项上,且不打断用户原本的 Tab 流——这点容易被忽略,但对键盘用户是决定性体验。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











