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
Product List
rubber-band exposed page

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.

layout.css
/* 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;
}
Support note: 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)

scroll-lock-legacy.js
// 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 });
Tip: Set the page body background to match your app's dark background colour — even if bounce occurs, the revealed area looks like your app rather than a jarring white gap.html { background: #0f0f0f; }

Browser Support

BrowserMin VersionStatusNotes
Safari iOSAllPartialoverscroll-behavior supported on iOS 16+; older: use touch event workaround
Safari iOS 16+16+Supportedoverscroll-behavior: none / contain fully supported
Chrome iOSAllPartialSame WebKit engine as Safari — same support level
Chrome Android63+SupportedFull overscroll-behavior support
Firefox59+SupportedFull overscroll-behavior support

Further Reading