The Problem
iOS uses a physics-based "elastic" scroll model. When a user scrolls to the very top or bottom of a scrollable container, momentum doesn't stop abruptly — the page stretches slightly and bounces back (rubber-band effect). When this happens inside a modal or bottom sheet, the bounce can "escape" into the parent page, revealing the background (often white) above or below your app shell.
Page background revealed by overscroll bounce
Root Cause
iOS's native UIScrollView uses an elastic scrolling model — scrolling past boundaries stretches the content and snaps back. This is called scroll chaining: when a child scroll container hits its limit, any remaining scroll momentum passes to the parent container (the page itself).
This causes two common visual bugs: (1) the page background "peeks" above the app shell, and (2) browser chrome or white space appears behind dark-themed apps.
Fix — overscroll-behavior (Modern)
The CSS property overscroll-behavior controls whether scroll chaining happens. Setting it to none on the page body prevents any bounce on the page itself. Using contain on inner containers allows them to still bounce, but keeps the effect contained.
/* Prevent the page from rubber-banding at all */
html,
body {
overscroll-behavior: none;
}
/* Let modal/sheet scroll still bounce within itself */
.sheet-content,
.modal-body {
overflow-y: auto;
overscroll-behavior-y: contain; /* bounce stays inside */
-webkit-overflow-scrolling: touch;
}overscroll-behavior landed in iOS Safari 16 (September 2022). For iOS 15 and below, you need the touchmove workaround below.Fix — touchmove preventDefault (Legacy iOS)
// For iOS 15 and below: prevent page bounce when a modal is at scroll boundary
const modal = document.querySelector('.modal-content');
let lastY = 0;
modal.addEventListener('touchstart', (e) => {
lastY = e.touches[0].clientY;
}, { passive: true });
modal.addEventListener('touchmove', (e) => {
const y = e.touches[0].clientY;
const scrollTop = modal.scrollTop;
const atTop = scrollTop <= 0 && y > lastY;
const atBottom = scrollTop + modal.clientHeight >= modal.scrollHeight && y < lastY;
if (atTop || atBottom) {
e.preventDefault(); // prevent chaining to page
}
lastY = y;
}, { passive: false });html { background: #0f0f0f; }Browser Support
| Browser | Min Version | Status | Notes |
|---|---|---|---|
| Safari iOS | All | Partial | overscroll-behavior supported on iOS 16+; older: use touch event workaround |
| Safari iOS 16+ | 16+ | Supported | overscroll-behavior: none / contain fully supported |
| Chrome iOS | All | Partial | Same WebKit engine as Safari — same support level |
| Chrome Android | 63+ | Supported | Full overscroll-behavior support |
| Firefox | 59+ | Supported | Full overscroll-behavior support |