dialog.showModal() 不能直接作为无障碍方案使用,必须手动实现焦点围栏、焦点管理与 backdrop 关闭逻辑,并确保 dialog 是 body 的直接子元素。

dialog.showModal() 不能直接当无障碍方案用
它确实自动加 aria-modal="true"、生成 ::backdrop、响应 Esc,但 Safari 全版本和旧 Edge 中焦点围栏完全失效:Tab 键仍会跳到页脚按钮,document.activeElement 可能还是 body。这不是“体验不好”,是键盘用户根本无法完成操作。
必须手动补三件事,缺一不可:
- 打开后立刻调用
firstFocusableElement.focus()——不能等 CSS 动画结束,也不能依赖autofocus(它只在元素挂载时生效,而showModal()不触发重挂载) - 监听
keydown拦截Tab和Shift+Tab,在模态框内手动循环焦点 - 关闭前缓存触发源元素(比如点击的
<button></button>),关闭后用triggerEl.focus()精准恢复,不是document.body.focus()
dialog 必须是 <body> 的直接子元素
Safari 中若把 <dialog> 嵌套在 <div class="container"> 里,::backdrop 可能压根不渲染;更严重的是,焦点捕获逻辑在部分浏览器中会退化为无隔离状态。
正确写法只有两条路:
立即学习“前端免费学习笔记(深入)”;
- 确保
<dialog id="my-dialog">是<body>的第一个层级子节点(即不能被任何 wrapper 包裹) - 始终用 JavaScript 控制:
dialog.showModal()打开,dialog.close()关闭;绝不要写dialog.open = true或dialog.style.display = 'block'
点击 backdrop 关闭必须手动监听
showModal() 生成的 ::backdrop 默认不响应 click 事件——这是规范行为,不是 bug。很多开发者以为点背景就能关,结果用户卡死。
基础监听写法:
dialog.addEventListener('click', e => {
if (e.target === dialog) dialog.close();
});
但 Safari 15.4–16.3 存在兼容问题:某些情况下 e.target 始终是 body。这时得 fallback 到坐标判断:
- 检查
e.clientX/e.clientY是否落在dialog.getBoundingClientRect()区域外 - 避免给
dialog设pointer-events: none,这会破坏原生焦点锁定
焦点元素必须真正可聚焦
第一个可聚焦元素必须是真正可交互的:<h3>、<p>、<div tabindex="-1"> 都不会被 focus() 激活。浏览器只聚焦原生可聚焦标签(button、a[href]、input、select)或显式声明 tabindex="0" 的元素。
实操建议:
- 用
requestAnimationFrame()包一层再聚焦,比setTimeout(0)更可靠 - 缓存焦点元素列表:
const focusables = dialog.querySelectorAll('button, input, select, [tabindex="0"]') - 关闭后恢复焦点时,务必确认
triggerEl仍存在于 DOM 且未被disabled或hidden
showModal(),而是让 Safari 用户按 Tab 不跳出去、让屏幕阅读器不读背景、让用户关窗后还能继续操作——这些细节不在 API 文档里,全靠手动兜底。



















