弹窗dom位置错乱导致aria-hidden失效,必须将弹窗容器直接置于末尾且不嵌套三层以上;焦点管理需手动接管activeelement;原生组件穿透须临时隐藏;移动端需用touchend并锁定滚动;dialog标签须为body直接子元素且domcontentloaded后调用showmodal。

弹窗DOM位置错乱导致aria-hidden失效
可访问性穿透最常发生在弹窗被塞进
必须把所有弹窗容器直接放在<main></main>末尾,不能嵌套三层以上。例如:<main><div id="modal-login" aria-hidden="true"></div></main> —— 这是唯一能保证语义流和焦点顺序正确的结构。
- 别用
<div onclick="">触发弹窗,必须用<code><button aria-controls="modal-id"></button> -
aria-hidden="true"和display: none必须同步开关,缺一不可 - 隐藏弹窗时,先
element.setAttribute('aria-hidden', 'true'),再element.style.display = 'none';显示时顺序反过来 - 多个弹窗共用同一
tabindex="-1"会导致焦点跳转混乱,每个弹窗根节点应设唯一tabindex="0" - 用
dialog原生标签时,showModal()后必须监听close事件,否则焦点不会自动释放 - 避免在弹窗内用
stopPropagation(),它会破坏Esc键和遮罩点击关闭逻辑 - 不要试图用
transform: translateZ(0)提升层级——对原生组件无效 - 若必须保留视频播放状态,可改用
video.pause()+poster占位图替代隐藏 - 地图组件需配合
map.hide()或销毁实例,仅靠display: none在某些SDK中不生效 - 禁止在遮罩层上监听
touchmove.prevent,否则弹窗内部滚动失效 - 还原
body样式时,必须清空style.cssText,残留top: -xxxpx会让后续滚动异常 - 键盘高度变化需监听
keyboardDidHide事件(iOS)或resize(Android WebView),动态调整弹窗bottom值
多弹窗堆叠时焦点管理断裂
连续打开登录弹窗→表单校验弹窗→确认提交弹窗,焦点会卡在第一个弹窗的关闭按钮上,后续弹窗无法接收键盘Tab或Esc,用户只能靠鼠标点——这是典型的焦点链断裂。
根本解法不是靠z-index堆高,而是用document.activeElement + focus()手动接管。每次showModal前,先记录当前焦点元素;弹窗关闭后,立刻调用triggerBtn.focus()回退,而不是依赖浏览器自动行为。
遮罩层被原生组件(video/map)穿透
哪怕z-index: 9999,<video></video>或<map></map>在iOS和部分安卓WebView中永远置顶,弹窗遮罩完全无效,点击穿透照旧发生。
唯一可靠方案是弹出前临时隐藏原生组件:给<video></video>加data-modal-safe标记,弹窗打开时统一设style.display = 'none';关闭后再恢复。不能只靠CSS visibility或opacity,它们不阻断可访问性树遍历。
移动端touchend穿透与键盘顶起叠加
用户在弹窗输入框聚焦后快速点击遮罩关闭,紧接着又点到底层按钮——这不是bug,是移动端300ms双击检测窗口+touchend未及时拦截共同导致的穿透链。
必须同时处理两层:一是用touchend替代click绑定关闭逻辑,二是关闭时立即执行body滚动锁定还原(含scrollTop缓存),否则键盘收起后页面留白错乱,底层元素暴露更明显。
dialog标签的DOM位置和执行时机——它必须是body的直接子元素,且showModal()调用必须在DOMContentLoaded之后。早一秒执行,Safari就静默失败;晚一步移除,backdrop残留会导致后续所有弹窗点击失效。











