You're blocking touchmove events to contain scroll. overscroll-behavior does it natively.
When a scrollable container hits its edge, the page behind it starts scrolling too. Developers block this with JavaScript event handlers or body overflow tricks. overscroll-behavior is the CSS property that turns off scroll chaining in one line.

When a user scrolls to the bottom of a modal and keeps scrolling, the page behind it starts moving. This is scroll chaining — the browser passes the scroll gesture up through the DOM when the focused container runs out of room. The traditional fix is JavaScript: intercept touchmove events, check whether the container has hit its boundary, and call preventDefault(). There's a CSS property for this.
overscroll-behavior controls what happens when a scroll gesture reaches the edge of a scrollable container. Three values:
auto— the default; scroll chains to the parent elementcontain— stops chaining at this element; overscroll effects (bounce, glow highlight) still rendernone— stops chaining and suppresses the overscroll effect entirely
One property on one element. The browser's own scroll machinery handles containment — no event listener, no JavaScript.
Here's the JavaScript approach, written carefully enough to actually work:
passive: false is the problem. Browsers optimistically start compositing the scroll on the GPU — when a listener is passive, they don't wait for it. The moment you declare passive: false, the browser has to park the entire scroll gesture, wait for your handler to finish, then decide whether to scroll. You pay that cost on every touchmove event, on every device, whether the user is anywhere near the edge or not. Chrome logs a warning about this in DevTools.
With CSS:
No passive penalty. No edge-case bugs where the user simultaneously hits the top and bottom boundary during a fast flick. No listener to clean up when the modal unmounts.
overscroll-behavior controls two other native gestures besides scroll chaining.
Pull-to-refresh on mobile Chrome. Overscrolling past the top of the page triggers the refresh gesture. If you've built a custom refresh indicator, or you're shipping an app shell where the native gesture doesn't belong, disable it on the document:
Use this deliberately. Pull-to-refresh is a familiar gesture — suppressing it at the body level surprises users on pages that function as documents. Reserve it for true app shells.
Back-navigation swipes on horizontal carousels. On macOS trackpads and some mobile browsers, swiping past the last card in a horizontal scroll container triggers browser back/forward navigation. This is almost never what you want:
The carousel scrolls freely within its bounds; going past the last slide does nothing instead of navigating away.
There are two axis-specific variants and a shorthand:
The most common pattern is overscroll-behavior-y: contain on vertically scrollable panels — modals, drawers, sidebars, dropdown bodies — where you want scroll to stay inside the component without disabling anything horizontally.
overscroll-behavior is Baseline 2019: Chrome 63 (December 2017), Firefox 59 (March 2018), Safari 16 (September 2022). iOS Safari was the last major holdout, shipping support in late 2022. It's been broadly available for several years across browsers, Node.js-powered WebViews, and PWA contexts.
Runs right in your browser — poke at it and watch the concept react live.
Think it clicked? Take the 7-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
Search your codebase for touchmove listeners that call e.preventDefault() to stop scroll from escaping a container — those are scroll-chaining patches. Replace the listener with overscroll-behavior: contain on the scrollable element and delete the JavaScript. While you're at it, add overscroll-behavior-x: contain to any horizontal scroll container where you don't want browser navigation triggered at the edges. The CSS path doesn't touch the browser's scroll machinery, scales to every device with no tuning, and removes a passive-false listener that was slowing down every scroll gesture regardless of whether it hit the boundary.
🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
Keep reading
One post a day, in your inbox
Each one with a runnable playground and a quiz. No pitch, no digest, unsubscribe in one click.
0 comments
Sign in to join the discussion, like comments, and save articles for later.