aria-modal="true"不能限制Tab键循环聚焦,仅向屏幕阅读器声明模态上下文;真正控制Tab行为的只有两种方式:一是使用原生<dialog>.showModal()(Chrome/Firefox/Safari 16.4+支持),二是手动实现焦点陷阱监听Tab/Shift+Tab并循环跳转。

aria-modal="true" 不能限制 Tab 键在模态框外部循环聚焦——它压根不控制任何原生键盘行为,只向辅助技术(如屏幕阅读器)声明“当前是模态上下文”。你看到 Tab 键逃出弹窗,不是配置漏了,而是指望它做它做不到的事。
为什么aria-modal="true"对Tab键完全无效
这个属性本质是语义标记,浏览器不会因此拦截或重定向焦点。实测中:
- Safari(所有版本)和旧版 Firefox 完全忽略它的焦点围栏意图
- 用户按 Tab 仍会落到
footer里的链接、页面顶部的导航菜单、甚至 iframe 外部元素 -
document.activeElement常停留在body,而非弹窗内首个可聚焦项
它唯一作用是让 NVDA/JAWS 等读屏软件暂停朗读背景内容——但键盘焦点照常乱跑。
真正能拦住Tab键的只有两种方式
必须二选一,不能混用或跳过:
立即学习“前端免费学习笔记(深入)”;
- 用原生
<dialog>元素调用.showModal():Chrome/Firefox/Safari 16.4+ 已深度支持,自动限制 Tab 范围、聚焦首个可聚焦子元素、响应 Escape 关闭并恢复焦点 - 手动实现焦点陷阱(focus trap):监听
keydown捕获Tab和Shift+Tab,动态计算当前聚焦位置,在可聚焦元素数组中首尾循环跳转
注意:focusin + contains() 是 fallback 方案,仅用于不支持 <dialog> 的旧环境(如 Safari focusout 更可靠,但性能开销略高,需提前缓存可聚焦列表。
手动焦点陷阱里最容易踩的三个坑
哪怕逻辑写对,细节错一点就失效:
- 可聚焦元素查询必须排除
tabindex="-1":正确写法是modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');别用不存在的:focusable伪类 - 首次打开后必须立刻
firstFocusable.focus(),不能等 CSS 动画结束——Safari 下动画未完成时调.focus()会被静默丢弃 - 关闭弹窗后焦点必须回到触发源(比如
id="open-settings-btn"),不是document.body;否则屏幕阅读器从头朗读,用户彻底丢失上下文
真正的难点不在“怎么写循环”,而在于触发源引用是否稳定、可聚焦元素集合是否实时准确、以及不同浏览器对 requestAnimationFrame 和 queueMicrotask 的响应差异——这些没法靠一个属性绕过去。



















