必须设成 menu,W3C ARIA 1.1 已废弃 aria-haspopup="true",仅支持 menu、listbox、tree、grid、dialog 五种值;设为 true 会导致语义错误及读屏器忽略子菜单触发逻辑。

aria-haspopup 该设成 true 还是 menu?
必须设成 menu,不是 true。W3C ARIA 1.1 规范已废弃 aria-haspopup="true",只保留 "menu"、"listbox"、"tree"、"grid"、"dialog" 这五种取值。设成 true 不仅语义错误,还会让部分读屏器(如 NVDA + Firefox)忽略子菜单触发逻辑。
常见错误现象:
- 键盘按
Enter或Space后子菜单不展开 - JAWS 读出“按钮,有弹出”,但没说明类型,用户无法预判是菜单还是对话框
实操建议:
- 一级菜单项(触发下拉)用
aria-haspopup="menu" aria-expanded="false" - 对应子菜单容器加
role="menu"和aria-labelledby指回触发元素 - 不要给
<button>同时加aria-haspopup和aria-controls——二者语义冲突,后者适用于region类型的可折叠内容
多级菜单里子菜单怎么正确挂载 DOM?
子菜单必须作为直接兄弟节点或逻辑上紧邻的后代出现,不能靠绝对定位“飞”到页面任意位置。否则屏幕阅读器会丢失父子关系,键盘导航(Tab / Arrow)会跳过整个子层级。
立即学习“前端免费学习笔记(深入)”;
使用场景:横向主菜单带二级垂直下拉,二级还有三级右向展开。
实操建议:
- 二级菜单放在一级
<li>内部,三级菜单放在二级<li>内部——保持 DOM 层级与视觉层级一致 - 用
position: absolute做视觉偏移没问题,但别用transform或display: none隐藏整块 DOM;隐藏应靠visibility: hidden+aria-hidden="true"配合 - 如果因布局限制必须脱离父容器(如挂到
body),必须用aria-owns显式声明归属关系,并确保焦点管理手动接管(否则 Tab 键会漏掉子菜单项)
键盘操作不支持方向键切换子菜单项?
根本原因是没实现 ARIA Menu Pattern 的焦点流逻辑。浏览器不会自动处理菜单内 ↑/↓、←/→ 导航,必须手写事件监听和焦点控制。
容易踩的坑:
- 只处理了
keydown,没阻止默认行为(event.preventDefault()),导致页面滚动或表单提交 - 在二级菜单里按
→想进三级,但没检查当前项是否有子菜单,直接跳到下一个同级项 - 用
tabindex="-1"控制可聚焦,但没在展开时主动.focus()到第一个菜单项,键盘用户得先按一次Tab才能进入
实操建议:
- 监听
keydown,对ArrowDown/ArrowUp在当前菜单内循环聚焦;ArrowRight先判断aria-haspopup="menu"是否存在,再展开并聚焦子菜单首项 - 每个菜单项用
role="menuitem",禁用项加aria-disabled="true"并跳过焦点 - 菜单收起时,把焦点移回触发它的按钮,而不是丢给
document.body
为什么 ChromeVox 读不出菜单层级关系?
因为缺少 aria-label 或 aria-labelledby 描述,或者用了冗余的 title 属性。ChromeVox 对 title 支持极弱,且会覆盖 ARIA 标签;而空的 aria-label="" 或未关联的 aria-labelledby 会让它回退到 innerText,丢失“二级菜单”这类上下文。
性能影响:过度嵌套 aria-label 字符串(比如拼接“文件 > 新建 > Word 文档”)不会拖慢渲染,但会让语音输出冗长难辨。
实操建议:
- 一级菜单项用简明
aria-label(如aria-label="文件菜单"),子菜单容器用aria-label="文件子菜单" - 避免在
<ul role="menu">上写aria-label,应该作用于每个role="menuitem" - 如果菜单项文字本身已含上下文(如“新建文档”),不必额外加“子菜单”字样——读屏器通过
role="menu"+aria-haspopup组合已能推断层级
最常被忽略的是:所有菜单项必须有唯一可访问的文字内容。用图标 <i class="icon-file"></i> 代替文字?必须补 aria-hidden="true" 并用 span sr-only 或 aria-label 提供文本。否则,菜单就只是“一堆听不见的方块”。











