aria-haspopup="dialog"必须作用于触发按钮,仅声明语义而非行为;弹窗容器须同时具备role="dialog"、aria-modal="true"、aria-labelledby,并配合手动焦点管理与键盘导航。

aria-haspopup="dialog" 的正确声明方式
它本身不声明弹窗,只是告诉辅助技术“这个元素会触发一个对话框”,真正的弹窗需要你手动实现 DOM 结构和交互逻辑。
常见错误是只加 aria-haspopup="dialog" 就以为屏幕阅读器能自动识别弹窗,结果弹出后焦点没管理、aria-modal="true" 没设、遮罩层缺失——辅助技术完全感知不到这是个模态对话框。
-
aria-haspopup="dialog"必须作用在触发按钮上(比如<button>或带role="button"的元素) - 弹窗容器需同时满足:
role="dialog"、aria-modal="true"、有aria-labelledby指向标题元素 - 必须手动控制焦点:点击按钮后,焦点要立即移到弹窗内部第一个可聚焦元素(如首项输入框或确认按钮)
- 按
Esc关闭时,焦点需回到原触发按钮,并重置aria-expanded(如果用了)
为什么不能只靠 aria-haspopup="dialog" 实现可访问弹窗
因为 aria-haspopup 是语义提示属性,不是行为指令。浏览器和屏幕阅读器不会因为它就自动创建、显示、管理或关闭弹窗。
实际影响很直接:JAWS/NVDA 读到按钮时会说“XXX,按钮,有弹出窗口”,但如果你的弹窗没设 role="dialog" 或没移焦点,用户根本不知道内容在哪、怎么操作、如何退出。
立即学习“前端免费学习笔记(深入)”;
-
aria-haspopup="dialog"和aria-expanded无关——它不表示当前展开状态,也不影响 DOM 渲染 - 若弹窗是纯 CSS
display: none切换,需配合aria-hidden="true/false"控制可访问性树可见性 - 没有
aria-modal="true"时,屏幕阅读器仍可朗读背景内容,违背模态对话框设计意图
最小可用示例(含关键可访问性要素)
<button aria-haspopup="dialog" aria-controls="my-dialog">打开设置</button> <div id="my-dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-hidden="true"> <h2 id="dialog-title">设置</h2> <input type="text" aria-label="用户名"> <button>保存</button> <button aria-label="关闭">×</button> </div>
注意:aria-controls 指向弹窗 ID 是良好实践(非强制),但真正起作用的是弹窗自身的 role 和 aria-modal;aria-hidden="true" 需在 JS 中动态切换,不能写死。
容易被忽略的焦点与键盘流细节
很多团队实现了弹窗 DOM 和 ARIA 标记,却卡在键盘导航这一环——这直接导致 WCAG 2.1 2.4.3 和 2.1.2 失败。
- 弹窗显示后,用
element.focus()聚焦到第一个可聚焦子元素,不要依赖autofocus(Safari 不可靠) - 实现
Tab键在弹窗内循环(不跳出),需监听keydown捕获Tab并手动控制焦点流向 - 关闭弹窗后,必须用
triggerButton.focus()把焦点还回去,否则键盘用户会迷失在页面空白处 - 移动端 VoiceOver 用户可能通过
Swipe Right/Left导航,所以弹窗结构必须语义完整、顺序合理
没做焦点管理的“对话框”,对屏幕阅读器用户来说,和消失了一样。



















