pointer-events: none仅禁用点击悬停等指针事件,不阻止touchmove滚动冒泡,底层能否滚动取决于自身overflow状态及是否拦截touchmove默认行为。

遮罩层加 pointer-events: none 为什么底层还是不能滚动?
因为 pointer-events: none 只影响鼠标/触摸的点击、悬停等事件,对 touchmove 和滚动行为完全无效。底层页面是否滚动,取决于它自身的可滚动状态(比如是否有 overflow-y: auto)和事件是否被冒泡触发,不是靠“透不透点”决定的。
常见错误是给遮罩设了 pointer-events: none,以为这样就能“让底层自由滚动”,结果手指一滑,页面照样跟着动——这不是遮罩没生效,是滚动事件根本没被拦截,而是从遮罩穿透到了 body。
- 遮罩必须覆盖整个视口(
width: 100vw; height: 100vh;),否则留白区域会成为滚动触发区 - 遮罩的
z-index要足够高,且不能被父级层叠上下文截断(比如父元素有transform或will-change) - 如果遮罩内含可滚动内容(如长列表),需单独处理其内部滚动,不能一刀切禁用所有 touchmove
移动端 touchmove 冒泡导致底层滚动,怎么精准拦截?
iOS 和多数安卓 WebView 中,遮罩上滑动时若未阻止默认行为,touchmove 会一路冒泡到 body,触发页面整体滚动。这是滚动穿透的根本原因,不是 CSS 定位失效。
正确做法是监听遮罩容器的 touchmove,但只在用户**不在可滚动子区域中滑动**时调用 e.preventDefault():
立即学习“前端免费学习笔记(深入)”;
- 必须传
{ passive: false },否则 Safari 会忽略preventDefault() - 用
e.target.closest('.scrollable-area')判断当前触摸点是否落在遮罩内可滚动区域(如.modal-content) - 仅当没命中滚动区才
e.preventDefault(),保证遮罩内内容仍可滚动
示例代码片段:
const mask = document.querySelector('.mask');
mask.addEventListener('touchmove', (e) => {
if (!e.target.closest('.mask-scrollable')) {
e.preventDefault();
}
}, { passive: false });
为什么 body { overflow: hidden } 在 iOS 上基本失效?
iOS Safari 对 body 的 overflow 属性支持极差:它既不阻止滚动,也不保持当前 scrollTop,甚至可能强制回弹到顶部。这不是 bug,是 WebKit 渲染机制决定的——body 不是标准滚动容器,它的溢出行为受视口和 documentElement 交叉影响。
- 不要把
overflow: hidden直接写死在标签上,容易被框架或第三方库覆盖 - 即使生效,也会导致滚动条消失、页面横向偏移(因宽度变化),且关闭后无法自动恢复位置
- 若项目用了
scroll-behavior: smooth,加overflow: hidden还可能卡住过渡动画
真正可靠的锁定方式:用 position: fixed 钉住 body
核心不是“让遮罩固定”,而是让页面内容失去滚动能力。把 body 自身设为 position: fixed,等于把它钉在视口里,自然无法再拖动。
关键操作顺序不能错:
- 显示遮罩前,立刻记录
document.scrollingElement.scrollTop(兼容body和documentElement) - 设置
document.body.style.position = 'fixed'和document.body.style.top = `-${scrollTop}px` - 隐藏遮罩时,先清除
position和top,再用requestAnimationFrame(() => window.scrollTo(0, scrollTop))恢复位置 - 避免对
html元素也设overflow: hidden,部分安卓 WebView 会干扰软键盘弹出后的 input 聚焦
最容易被忽略的是:Safari 中 scrollTop 恢复有 1 帧延迟,不用 requestAnimationFrame 或 setTimeout(..., 0) 包一层,页面大概率会闪一下或跳位置。


















