aria-haspopup="listbox"必须配合aria-controls指向唯一存在的role="listbox"容器id,且需同步aria-expanded状态与listbox显隐逻辑,否则屏幕阅读器无法识别弹出内容及状态。

aria-haspopup="listbox" 本身不自动关联选择列表
设成 listbox 只是告诉屏幕阅读器“这个元素会弹出一个列表式控件”,但不会自动把按钮和它下面的 <ul></ul> 或 <div role="listbox"> 关联起来。屏幕阅读器需要明确知道哪个元素是弹出内容的容器,否则只会读“按钮,有弹出菜单”,却不知道弹出的是什么、在哪、是否已展开。
<h3>必须用 aria-controls 指向 listbox 元素的 id</h3>
<p>这是最关键的一步。没有 <code>aria-controls,aria-haspopup="listbox" 几乎等于无效:
-
aria-controls的值必须是目标listbox元素的id,且该id必须唯一、存在、可访问(不能 display: none 或 aria-hidden="true") - 目标元素需显式声明
role="listbox",仅靠<select></select>或<ul></ul>不够 - 如果 listbox 初始隐藏,建议用
aria-expanded="false"同步控制按钮状态,避免屏幕阅读器误判为已打开
<button aria-haspopup="listbox" aria-controls="my-listbox" aria-expanded="false">选择颜色</button> <div id="my-listbox" role="listbox" aria-hidden="true"> <div role="option" tabindex="-1">红色</div> <div role="option" tabindex="-1">蓝色</div> </div>
listbox 内容必须满足可聚焦和语义结构
即使关联上了,如果内部不可操作,屏幕阅读器仍无法正确导航或播报选项:
- 每个选项必须有
role="option",且父容器是role="listbox" - 选项需支持键盘聚焦(通常加
tabindex="-1"),否则 JAWS/NVDA 可能跳过 - 避免用
<li>直接包裹option却不声明role="list"—— 这会干扰 listbox 的层级语义 - 不要在 listbox 上同时用
aria-hidden="true"和display: none:前者会让屏幕阅读器彻底忽略内容,后者影响视觉焦点流
动态显示时要同步更新 aria-expanded 和 aria-hidden
点击按钮展开 listbox 后,光改 CSS 不够,必须手动同步 ARIA 状态:
- 展开时:设置按钮
aria-expanded="true",listbox 移除aria-hidden="true"(或设为false) - 收起时:反向操作;若用
display: none隐藏 listbox,务必同时设aria-hidden="true",否则焦点可能进入不可见区域 - 某些旧版 NVDA 对
aria-expanded变化响应延迟,可加setTimeout微调顺序,但优先确保 DOM 更新与 ARIA 属性更新在同一渲染帧
role="listbox" 声明,或者 aria-controls 指向了一个不存在或未渲染的 id —— 这类问题在 React/Vue 动态渲染场景下特别常见,且错误非常安静,没有任何报错提示。











