aria-haspopup 的正确取值仅有 "menu"、"listbox"、"tree"、"grid"、"dialog",其中下拉导航菜单应使用 "menu";设为 true/false 或无值均无效,且必须配合 role="button"、tabindex="0"(如非原生按钮)及子菜单的 role 和 aria-labelledby 才能正确工作。

aria-haspopup 的正确取值有哪些
aria-haspopup 不是布尔开关,它的值必须明确说明子菜单的类型。设成 true 或 false 是无效的,屏幕阅读器会忽略或误读。实际可用值只有:"menu"、"listbox"、"tree"、"grid"、"dialog" —— 其中下拉式导航菜单几乎总是用 "menu"。
常见错误是写成:aria-haspopup="true",这在 WAI-ARIA 1.1+ 中已被标记为过时;更糟的是写成 aria-haspopup(无值),HTML 解析后等价于 aria-haspopup="",多数读屏软件直接跳过。
- 按钮触发横向二级菜单 → 用
aria-haspopup="menu" - 按钮展开一个带滚动、可键盘操作的选项列表 → 可考虑
"listbox",但需配套role="listbox"和完整键盘交互逻辑 - 避免用
"true"、"false"、"1"等非标准字符串
必须配合 role="button" 和键盘可聚焦性
仅加 aria-haspopup 不足以让屏幕阅读器识别这是“可展开的按钮”。如果元素本身不是语义化按钮(比如用了 <div> 或 <code><span></span>),必须显式声明 role="button",否则 NVDA/JAWS 会把它当普通文本读,不提示“已启用”或“有子菜单”。
同时,该元素必须能获得焦点:要么是原生 <button></button>,要么是添加了 tabindex="0" 的自定义控件。否则键盘用户无法到达它,读屏软件也不会主动播报 aria-haspopup 属性。
- ✅ 推荐:
<button aria-haspopup="menu">产品</button> - ✅ 可行但需谨慎:
<div role="button" aria-haspopup="menu" tabindex="0">产品</div> - ❌ 无效:
<span aria-haspopup="menu">产品</span>(不可聚焦 + 无 role)
子菜单容器要配 role 和 aria-labelledby
aria-haspopup 只告诉用户“这里能弹出东西”,但不指明弹出什么、跟哪个元素关联。必须让子菜单容器具备明确角色,并通过 aria-labelledby 反向绑定到触发按钮,否则读屏软件无法建立父子关系,用户打开后不知道这是谁的菜单。
子菜单通常用 <ul></ul> 实现,此时应设 role="menu"(而非默认的 list),且每个菜单项用 role="menuitem"。更重要的是,子菜单容器需加上 aria-labelledby="触发按钮的 id"。
- 触发按钮加
id="products-btn" - 子菜单加
role="menu" aria-labelledby="products-btn" - 若子菜单初始隐藏,还需搭配
aria-hidden="true",展开时改为aria-hidden="false"(不能只靠display: none)
别忘了键盘交互和焦点管理
即使 HTML 属性全对,如果用户按 Enter 或 Space 无法展开菜单,或展开后焦点没落到第一个菜单项,aria-haspopup 就成了摆设。读屏用户依赖键盘流完成操作,而不仅仅是听描述。
典型问题包括:菜单展开后焦点仍卡在按钮上、按 Esc 不能收起、方向键无法在菜单项间移动。这些都属于 ARIA 实践的一部分,不是 aria-haspopup 自己能解决的。
- 监听
keydown,对Enter/Space触发展开,Esc收起 - 展开后立即
menuElement.firstElementChild?.focus() - 菜单项需支持
ArrowDown/ArrowUp切换,Home/End跳转首尾
属性只是信标,行为才是路径。漏掉任一环节,盲人用户看到的就只是“有个按钮,听说它有菜单”,然后卡住。











