How to Stop Scroll‑Penetration in Mobile Web Overlays
This article explains why scroll‑penetration occurs when a modal mask covers the page, why simple overflow:hidden or event‑bubbling tricks fail, and provides a complete solution using passive event listeners, selective default‑preventing, multi‑layer handling, and a ready‑to‑use React component.
When a mask layer covers the
bodyand is scrolled, the underlying page scrolls as well, a phenomenon called scroll‑penetration.
What Is Scroll Penetration?
Scrolling the mask triggers scrolling of the content underneath because the browser’s native scroll handling propagates the scroll event to the
Documenttarget, not because of event bubbling.
Is Stopping Propagation Enough?
Listening to
scroll/
touchmoveand calling
stopPropagationdoes not help; scroll events on normal elements do not bubble, and the browser’s pending scroll queue processes them according to the W3C spec.
Bubbles not on elements, but bubbles to the default view when fired on the document.
Therefore, scroll‑penetration is not a bug but expected behavior: the scroll principle is
scroll for what can scroll, independent of CSS positioning.
Adding overflow:hidden ?
Setting
overflow:hiddenon
bodyprevents scrolling on desktop, but on mobile the body can still scroll if its height exceeds the viewport.
<code>html, body {
overflow: hidden;
}</code>To keep the current scroll position, record
scrollTopbefore applying
overflow:hiddenand restore it afterward.
Preventing Body’s Default Scroll?
Adding a
touchmovelistener on
documentwith
preventDefaultworks on iOS but fails on Android because Chrome’s default passive listeners ignore
preventDefault.
<code>document.addEventListener('touchmove', e => {
e.preventDefault();
}, { passive: false });</code>Detect passive support before using the object syntax:
<code>var supportsPassive = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function() { supportsPassive = true; }
});
window.addEventListener('test', null, opts);
} catch (e) {}
</code>Selective Prevention for Scrollable Elements
Mark scrollable containers with a
can-scrollclass and only prevent default on touches that are not inside those elements:
<code>document.addEventListener('touchmove', e => {
const excludeEl = document.querySelectorAll('.can-scroll');
const isExclude = [].some.call(excludeEl, el => el.contains(e.target));
if (!isExclude) {
e.preventDefault();
}
}, { passive: false });</code>When the user reaches the top or bottom of a scrollable element, prevent the default to stop penetration:
<code>let initialY = 0;
scrollEl.addEventListener('touchstart', e => {
if (e.targetTouches.length === 1) {
initialY = e.targetTouches[0].clientY;
}
});
scrollEl.addEventListener('touchmove', e => {
if (e.targetTouches.length !== 1) return;
const deltaY = e.targetTouches[0].clientY - initialY;
if ((scrollEl.scrollTop + scrollEl.clientHeight >= scrollEl.scrollHeight && deltaY < 0) ||
(scrollEl.scrollTop <= 0 && deltaY > 0)) {
e.preventDefault();
}
}, { passive: false });
</code>Supporting Multiple Overlays
Maintain a
Setof locked overlays; only when the set is empty should the global scroll listener be removed:
<code>const lockedList = new Set();
function lock() { lockedList.add(this); }
function unlock() {
lockedList.delete(this);
if (lockedList.size === 0) {
// remove global listeners
}
}
</code>React Component Wrapper
A ready‑to‑use React component creates a
LockScrollinstance on mount and calls
lockor
unlockbased on the
lockprop:
<code>componentDidMount() {
const opts = this.props.selector || undefined;
this.lockScroll = new LockScroll(opts);
this.updateScrollFix();
}
updateScrollFix() {
const { lock } = this.props;
if (lock) {
this.lockScroll.lock();
} else {
this.lockScroll.unlock();
}
}
componentDidUpdate(prevProps) {
if (prevProps.lock !== this.props.lock) {
this.updateScrollFix();
}
}
componentWillUnmount() {
this.lockScroll.unlock();
}
</code>Usage:
<code><ScrollFix lock={show}>
{/* modal content */}
</ScrollFix>
</code>By wrapping any modal with
ScrollFixand toggling the
lockprop, developers can avoid scroll‑penetration without worrying about the underlying implementation.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.