原生<dialog>需手动补全焦点管理;必须实现焦点捕获、循环与回归,否则Safari/旧Edge中键盘用户会跳出对话框。

原生 <dialog> 是结构与无障碍的起点,但仅靠它不能解决焦点管理问题;必须手动补全焦点捕获、循环和回归逻辑,否则键盘用户在 Safari 或旧版 Edge 中会直接“溜出”对话框。
为什么 <dialog> 不能只靠 showModal() 就万事大吉
这个 API 确实自动设置了 aria-modal="true"、创建了 ::backdrop、响应 Escape 键,但它在 Safari(包括 iOS/macOS 全版本)和部分旧版 Edge 中不触发真正的焦点围栏。实测中:document.activeElement 可能仍停留在 body,Tab 键也能跳到页脚按钮上——这不是体验差,是功能失效。
所以必须主动做三件事:
- 打开后立刻聚焦首个可聚焦元素(不能等 CSS 动画结束)
- 监听
keydown拦截 Tab/Shift+Tab,在模态框内手动循环 - 关闭前缓存触发源,并在关闭后精准恢复其焦点(不是
document.body.focus())
HTML 结构必须带语义和可聚焦锚点
一个合格的模态框根节点至少要同时满足:role="dialog"、aria-modal="true"、aria-labelledby 和 aria-describedby。标题和描述元素需有明确 ID,且不能是纯文本节点。
立即学习“前端免费学习笔记(深入)”;
示例结构:
使用 PptxGenJS 和 Python 解析,将结构化的 HTML/CSS 幻灯片内容转换为原生、可编辑的 PowerPoint(.pptx)演示文稿。
<button id="open-settings-btn">设置</button> <dialog id="settings-dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describedby="dialog-desc"> <h2 id="dialog-title">系统设置</h2> <p id="dialog-desc">请调整以下选项以优化您的使用体验。</p> <input type="text" placeholder="用户名"> <button>保存</button> <button id="close-settings-btn">取消</button> </dialog>
注意:<dialog> 内部必须至少有一个可聚焦元素(如 <input>、<button> 或带 tabindex="0" 的容器),否则首次打开时无法自动聚焦,屏幕阅读器也会跳过整个区域。
手动实现焦点围栏的最小可行代码
核心是获取所有合法可聚焦子元素,并在 Tab 键按下时计算索引并跳转。别用 :focusable(浏览器不支持),也别漏掉 [href] 这类可聚焦链接。
关键点:
- 过滤器必须写全:
button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' - 首次聚焦用
requestAnimationFrame(() => first.focus()),确保 DOM 已挂载且样式生效 - 监听事件绑定在
<dialog>上而非document,避免多层模态时冲突 - 关闭时检查触发源是否还存在、是否可聚焦,再调用
.focus();若已销毁,退回到document.getElementById('open-settings-btn')这类显式引用
嵌套模态框时焦点管理容易被忽略的细节
当 #modal-1 打开后又触发 #modal-2,必须暂停前者的焦点监听器,否则两个围栏会互相干扰:比如按 Tab 到 #modal-2 最后一个元素时,#modal-1 的监听器可能抢先把焦点塞回自己第一个元素。
更隐蔽的问题是 aria-modal 的层级覆盖:Safari 不支持嵌套 aria-modal="true",此时应把外层模态框设为 aria-modal="false" 并手动控制其 aria-hidden="true",只让最内层保持 aria-modal="true"。否则屏幕阅读器会混淆上下文,朗读顺序错乱。


















