弹窗打开后焦点未进入且读屏无反应,根本原因是缺少role="dialog"、aria-modal="true"及手动首次聚焦;必须显式设置语义属性并用requestAnimationFrame延迟聚焦首个可聚焦元素,否则键盘用户无法进入、屏幕阅读器不朗读内容。

弹窗打开后焦点没进到里面,读屏软件完全没反应
这是最常见的无障碍失效现象:用户点击按钮,弹窗出现,但焦点仍停留在原按钮上,屏幕阅读器不朗读弹窗内容,键盘 Tab 也无法进入弹窗内部。
根本原因在于缺少语义角色和焦点控制逻辑。原生 <dialog> 调用 showModal() 会自动处理焦点锁定和 ARIA 暴露,但自定义弹窗几乎从不这么做。
- 必须显式设置
role="dialog"或role="alertdialog"(后者用于紧急提示,会中断当前语音) - 需添加
aria-modal="true"告知辅助技术“此元素是模态的”,否则读屏器可能继续读取背景内容 - 弹窗打开后,立即调用
focus()到第一个可聚焦子元素(如<button>、<input>),不能依赖用户手动 Tab - 若首个可聚焦元素是
<button>,建议加autofocus属性(<button autofocus>),但注意 Safari 对 autofocus 的支持不稳定,JS fallback 更可靠
Tab 键跳出弹窗、Esc 关不掉、点遮罩没响应
这些不是“功能缺失”,而是焦点管理与事件监听没对齐标准行为。现代 <dialog> 默认支持 Esc 关闭、点击 backdrop 关闭、Tab 循环锁定——但仅当它没被父容器截断时。
-
showModal()自动禁用背景交互,但若弹窗外层有overflow: hidden或transform(比如低代码画布容器),backdrop 渲染会被裁剪,导致点击无效 - 自定义弹窗必须手动实现 Tab 循环:监听
keydown,检测Tab键,当焦点移到最后一个可聚焦元素再按 Tab 时,强制 focus 到第一个;Shift+Tab 同理反向 - Esc 关闭需监听
keydown并判断event.key === 'Escape',且确保该监听绑定在弹窗自身或document上(避免被中间事件代理拦截) - 点击遮罩关闭,不要只监听遮罩
click——应监听整个 backdrop 区域,或更稳妥地监听dialog::backdrop伪元素(仅限原生<dialog>)
旧浏览器降级时 aria-hidden 和 hidden 混用出问题
Chrome 97+/Edge 97+/Firefox 98+ 支持 <dialog>,但 Safari 15.6 之前和所有 IE 版本不支持。降级方案不是简单切 display: none,而是语义与行为双同步。
立即学习“前端免费学习笔记(深入)”;
- 用
hidden属性比display: none更安全:它同时移除视觉渲染和辅助技术暴露,而display: none不保证屏幕阅读器忽略内容 - 若必须用
aria-hidden="true",请确保它和hidden不共存——两者语义冲突,部分读屏器优先级混乱 - 降级时,
<body>必须加inert属性(或用 JS 模拟:遍历所有可聚焦元素设tabindex="-1"),否则键盘用户仍能 Tab 到背景按钮 - 别忘了恢复:弹窗关闭后,要移除
inert、还原tabindex、并将焦点设回触发按钮(triggerBtn.focus()),否则用户迷失在页面中
为什么用 <dialog> 还要手写焦点循环和 aria-label?
<dialog> 确实省去了 backdrop、z-index、禁用背景等样式和逻辑,但它不自动解决所有无障碍细节。尤其当弹窗内容动态生成,或包含无文本图标按钮时,问题立刻浮现。
-
<dialog>不自动为关闭按钮添加aria-label,如果只放一个×,读屏器会读成“乘号”而非“关闭对话框” - 没有标题的
<dialog>会被读成“对话框”,缺乏上下文。必须用aria-labelledby指向内部<h3>,或用aria-label直接声明 - 表单类弹窗(如登录)建议用
<form method="dialog">,这样点击任意<button type="submit">会自动触发close()并返回returnValue,比手绑 click 更健壮 - 真正容易被忽略的是「状态通知」:弹窗打开后,应触发
aria-live="polite"区域播报“对话框已打开,按 Esc 键关闭”,否则读屏用户无法感知状态变化



















