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 body and 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 Document target, not because of event bubbling.
Is Stopping Propagation Enough?
Listening to scroll / touchmove and calling stopPropagation does 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:hidden on body prevents scrolling on desktop, but on mobile the body can still scroll if its height exceeds the viewport.
html, body {
overflow: hidden;
}To keep the current scroll position, record scrollTop before applying overflow:hidden and restore it afterward.
Preventing Body’s Default Scroll?
Adding a touchmove listener on document with preventDefault works on iOS but fails on Android because Chrome’s default passive listeners ignore preventDefault.
document.addEventListener('touchmove', e => {
e.preventDefault();
}, { passive: false });Detect passive support before using the object syntax:
var supportsPassive = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function() { supportsPassive = true; }
});
window.addEventListener('test', null, opts);
} catch (e) {}Selective Prevention for Scrollable Elements
Mark scrollable containers with a can-scroll class and only prevent default on touches that are not inside those elements:
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 });When the user reaches the top or bottom of a scrollable element, prevent the default to stop penetration:
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 });Supporting Multiple Overlays
Maintain a Set of locked overlays; only when the set is empty should the global scroll listener be removed:
const lockedList = new Set();
function lock() { lockedList.add(this); }
function unlock() {
lockedList.delete(this);
if (lockedList.size === 0) {
// remove global listeners
}
}React Component Wrapper
A ready‑to‑use React component creates a LockScroll instance on mount and calls lock or unlock based on the lock prop:
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();
}Usage:
<ScrollFix lock={show}>
{/* modal content */}
</ScrollFix>By wrapping any modal with ScrollFix and toggling the lock prop, developers can avoid scroll‑penetration without worrying about the underlying implementation.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
