showmodal() 不保证首次聚焦,需手动实现:等渲染(requestanimationframe)、找可聚焦元素(button/[href]/input等)、聚焦并兜底(dialog.tabindex=-1);还需处理tab循环和关闭后焦点回归。

dialog.showModal() 会自动尝试聚焦第一个可聚焦子元素,但这不是“保证聚焦”,而是“尝试一次”——失败就放弃,焦点常落到 body 上,尤其在 Safari、异步内容、无有效聚焦项时。
为什么showModal()后焦点还是跑到了页面顶部或地址栏
因为浏览器只在 showModal() 调用瞬间查一次可聚焦元素,不等 DOM 渲染完成,也不重试。常见失效场景:
- 弹窗内容由
fetch加载后插入,showModal()时 DOM 还是空的 - 内部只有
<div tabindex="-1">,没有 <code>button、input等原生可聚焦标签 - 用了
display: none或visibility: hidden包裹子元素,getComputedStyle判定为不可见,被过滤掉 - Safari 15.4+ 虽支持
showModal(),但首次聚焦逻辑极脆弱,常 fallback 到<dialog></dialog>自身(而它默认不可键盘聚焦) - 用
requestAnimationFrame()包裹查找逻辑,确保 DOM 已挂载且样式计算完成 - 用这个选择器找合法可聚焦项:
button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])—— 不要只查button,漏掉带tabindex="0"的容器会导致屏幕阅读器卡住 - 如果没找到,给
<dialog></dialog>自身设tabindex="-1"再.focus(),至少让它能接收键盘事件 - 聚焦前检查
document.activeElement !== target,避免重复聚焦干扰用户已有操作 - Tab 循环不能监听
keydown拦截 —— 会破坏 NVDA/JAWS 的内置导航键(如 Insert+F7),正确做法是监听focusin,当焦点即将离开<dialog></dialog>时,强制跳回首/尾元素 - 关闭前必须缓存触发按钮的引用,不能只存
querySelector('#trigger')返回的节点 —— SPA 路由切换后该节点可能已从 DOM 移除,.focus()静默失败 - 推荐用
el.addEventListener('click', handler, { once: true })绑定触发逻辑,或在close()前显式调用removeEventListener(),避免多个弹窗叠加时监听器互相干扰
怎么写一个真正能落地的首次聚焦逻辑
不能依赖 showModal() 的自动行为,必须手动接管。关键三步:等渲染、找元素、聚焦并兜底。
Tab 键循环和关闭后焦点回归必须手动实现
showModal() 完全不管 Tab 循环,也不管关闭后焦点回哪。不处理这两点,WCAG 2.1 AA 直接不合规。
最易被忽略的一点:很多人以为只要用了 <dialog></dialog> 就算完成了焦点管理。实际上,showModal() 只是起点,后续所有焦点流转都得你亲手控制——包括首次聚焦时机、循环边界、关闭归位,缺一不可。否则在 Safari 和主流屏幕阅读器下,用户一按 Tab 就“掉出”弹窗,根本没法完成表单操作。











