React单页应用Safari滚动穿透应采用Fixed定位+滚动快照方案:弹窗前记录scrollTop,设body为fixed并用负top抵消跳顶;关闭时还原样式并重置滚动位置,兼顾iOS 12~17及WKWebView兼容性。
用户在iOS Safari中打开弹窗时,手指在弹窗区域滑动,底层页面仍会跟着滚动,甚至触发橡皮筋回弹,导致上下文丢失、操作错乱。这不是bug,而是Safari将滚动视为viewport级行为,仅靠CSS overflow: hidden完全无效。
在弹窗打开后,用Safari开发者工具远程调试,检查document.scrollingElement.scrollTop是否在滑动过程中持续变化——如果变化,说明穿透已发生;如果不变,问题可能出在其他环节(比如弹窗内部未设overflow-y: auto)。
这一步必须做,避免把flex布局错位、事件监听遗漏等问题误判为滚动穿透。
方法一:在遮罩层上阻止touchmove
给遮罩DOM节点绑定touchmove并调用preventDefault:
modalRef.current.addEventListener('touchmove', e => e.preventDefault(), { passive: false });
⚠️ 注意:此法会同时禁用弹窗内所有可滚动区域(如长列表、textarea)的滑动,iOS 11+即使加了{ passive: false }也可能静默忽略。
方法二:条件式拦截(推荐)
只在触摸点不在可滚动子元素内时才阻止:
const isScrollable = (el) => el?.matches('.modal-scrollable, .popup-list, textarea') || el?.closest('.modal-scrollable, .popup-list, textarea');
modalRef.current.addEventListener('touchmove', e => { if (!isScrollable(e.target)) e.preventDefault(); }, { passive: false });
这是Ant Design Mobile、Vant等主流UI库实际采用的方案,兼容iOS 12~17、WKWebView及第三方内嵌WebView。
第一步:弹窗打开前,记录当前滚动位置
const scrollTop = document.scrollingElement.scrollTop;
第二步:设置body为fixed定位,并用负top抵消跳顶
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollTop}px`;
document.body.style.width = '100%';
第三步:弹窗关闭时,立即还原滚动位置并清除样式
document.body.style.position = '';
document.body.style.top = '';
document.body.style.width = '';
document.scrollingElement.scrollTop = scrollTop;
document.scrollingElement.offsetHeight; // 强制重排,防止Android WebView延迟生效