纯CSS :hover菜单对键盘用户不可用,因浏览器无法识别其逻辑菜单结构,导致Arrow键无法流转焦点,屏幕阅读器仅将其视为独立列表项而非菜单上下文。

必须手写键盘导航逻辑,浏览器不会自动处理多级菜单的 Arrow 键聚焦流转。
为什么纯 CSS :hover 的菜单对键盘用户完全不可用
纯靠 :hover 显示子菜单时,Tab 进入一级项后,按 → 无法进入二级,按 ↓ 也无法在当前级循环聚焦 —— 因为浏览器根本不知道这些 li 和 a 构成一个逻辑菜单。屏幕阅读器只看到一堆独立列表项,没有“菜单”上下文。
- 原生
select自带完整键盘支持(ArrowUp/Down 切换选项,Enter 确认),但仅限单层 -
details/summary支持 Space/Enter 展开,但不支持多级嵌套(Android WebView 中易失效) - 自定义多级菜单必须手动监听
keydown,并按 WAI-ARIA Menu Pattern 实现焦点控制
关键 ARIA 角色与属性怎么配才有效
错配 aria-haspopup 或漏掉 role="menuitem" 会导致读屏器跳过整个子菜单,或误读为按钮/区域。
- 可展开的
li或a必须设aria-haspopup="menu"(不是"true")和aria-expanded="false" - 子菜单容器
ul必须设role="menu"和aria-labelledby(值为触发元素的id) - 每个可操作菜单项用
role="menuitem";分隔线用role="separator" - 禁用项必须同时设
aria-disabled="true"和tabindex="-1",不能只灰掉样式
Arrow 键聚焦流转的手写逻辑要点
不能依赖浏览器默认行为,得在 role="menu" 容器内手动计算焦点位置。
立即学习“前端免费学习笔记(深入)”;
- 监听
keydown,捕获ArrowDown/ArrowUp:聚焦下一个/上一个role="menuitem" -
ArrowRight:若当前项有子菜单且已展开,直接聚焦其第一个menuitem -
ArrowLeft:收起当前子菜单,并将焦点移回父级触发项 - Enter/Space:触发展开/收起,或执行菜单项动作(需区分是否为可展开项)
- Esc:逐级收起,焦点回到最外层触发按钮
focusout 与 blur 的坑比想象中深
想实现“点空白处收起”,只监听 document.addEventListener('click', ...) 会误关自己 —— 因为点击下拉按钮本身也会冒泡触发。
- 正确做法:监听
mousedown,检查event.target是否在菜单容器外 - 同时监听触发元素的
focusout,延迟 100ms 关闭(防 Chrome 中快速切换输入框失焦误关) - Firefox 中
focusout不冒泡,blur又不触发,单靠一个事件必翻车 - 移动端要额外监听
touchstart,避免 iOS Safari 的 300ms click 延迟导致响应滞后
真正难的不是让菜单“看起来能用”,而是让键盘用户在三级、四级菜单间不迷失 —— 每一层的 position: relative、每一处 tabindex 的取舍、每一次 focus() 的时机,都得和 DOM 结构、CSS 定位、ARIA 语义咬合严丝合缝。漏掉任意一环,无障碍就断在那一层。



















