dialog.showModal()不能直接作为无障碍方案,因Safari和旧Edge中焦点围栏失效,需手动处理焦点锁定、Tab循环及触发源恢复,且必须确保dialog为body直系子元素并用JS控制开关。
它确实自动加 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 open></dialog> 或 style="display: block" 完全绕过原生模态逻辑:没遮罩层、Esc 不响应、背景仍可点击、焦点不锁定、aria-modal 不生效。更糟的是,Safari 中若把 <dialog> 嵌套在 <div class="container"> 里,::backdrop 可能压根不渲染。
正确做法只有两条路:
立即学习“前端免费学习笔记(深入)”;
<dialog> 是 <body> 的直接子元素dialog.showModal() 打开,dialog.close() 关闭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"]')
if (triggerEl && triggerEl.offsetParent !== null) triggerEl.focus()
最易被忽略的点:Safari 对 dialog 的 DOM 位置极其敏感——哪怕多一层 wrapper,::backdrop 就可能消失;而焦点循环逻辑一旦漏掉 Shift+Tab 分支,键盘用户就会卡在最后一个元素上出不去。