CSS
    scrollfrontendlayout

    Controlling Scroll Boundaries with overscroll-behavior

    Controlling Scroll Boundaries with overscroll-behavior

    Editor: Paul RadfordJun 28, 20267 min read
    Controlling Scroll Boundaries with overscroll-behavior

    The overscroll-behavior CSS property lets you stop scroll input from propagating past an element's own scroll boundary into its parent or the browser window. Set it to contain on a scrollable container to kill scroll chaining and pull-to-refresh bounce, without touching JavaScript or disabling scrolling itself.

    Why does scrolling a modal move the page behind it?

    This is the problem overscroll-behavior was built to fix. When you scroll a nested container, like a chat panel, a modal, or a sidebar, and the user hits the top or bottom of that container's content, browsers historically kept honoring the remaining scroll delta by handing it to the parent element. Scroll a chat window to its last message, keep swiping, and the whole page jumps. That handoff is called scroll chaining, and it's almost always unwanted from a UX standpoint. Users perceive it as a bug, because visually the modal looks like a closed box, but the input leaks through it.

    The old fixes were all hacks: JavaScript scroll listeners that call preventDefault(), wheel event math, or the "scroll lock body" pattern that swaps overflow: hidden on <body> when a modal opens (and then has to carefully restore scroll position on close, often flickering on iOS). overscroll-behavior replaces most of that with one CSS declaration.

    Basic syntax and values

    css
    1/* Applies to the element's own scroll boundary */
    2.modal-content {
    3 overflow-y: auto;
    4 overscroll-behavior: contain; /* stop chaining to ancestors */
    5}

    The property accepts:

    • auto (default): normal chaining and native effects like pull-to-refresh or rubber-banding continue.
    • contain: scroll chaining to ancestor scrollers is blocked, but this element still shows its own native overscroll effects (like the rubber-band bounce on iOS or the Android glow).
    • none: same as contain, but also suppresses the native overscroll effect on the element itself.

    There are also longhand axis-specific properties, overscroll-behavior-x and overscroll-behavior-y, useful when a container only scrolls in one direction and you want chaining blocked on that axis without affecting the other.

    css
    1/* A horizontally scrolling carousel that shouldn't
    2 drag the page sideways on trackpads, but should still
    3 allow vertical page scroll to pass through untouched */
    4.carousel-track {
    5 overflow-x: auto;
    6 overscroll-behavior-x: contain;
    7 overscroll-behavior-y: auto;
    8}

    Stopping pull-to-refresh without disabling scroll

    A common request: kill the "pull down at the top of the page and see a refresh spinner or a visual stretch" behavior on mobile Chrome and some PWAs, without setting touch-action: none (which also kills scrolling entirely) or writing touch event handlers.

    css
    1/* Applied to the scrolling root. Blocks the browser's native
    2 pull-to-refresh / bounce gesture at the very top of the page,
    3 while leaving normal scrolling untouched. */
    4html, body {
    5 overscroll-behavior-y: contain;
    6}

    Using none instead of contain here also removes the visual glow/bounce effect itself on the elements where the browser renders it, which some teams prefer for a more "native app" feel, especially in installed PWAs where a browser chrome refresh spinner looks out of place.

    Where this actually matters: modals, drawers, and chat UIs

    The highest-value use case is any fixed-position or overlay UI that sits on top of a scrollable page: modals, off-canvas navigation drawers, bottom sheets, and chat message lists. Without containment, a user scrolling inside the overlay, once they reach the end of its content, will drag the underlying page instead. This is jarring specifically because the overlay usually has a backdrop that visually implies the page below is inert.

    html
    1<!-- A typical modal shell -->
    2<div class="backdrop">
    3 <div class="modal" role="dialog" aria-modal="true">
    4 <div class="modal-body">
    5 <!-- long scrollable content -->
    6 </div>
    7 </div>
    8</div>
    css
    1.modal-body {
    2 max-height: 70vh;
    3 overflow-y: auto;
    4 overscroll-behavior-y: contain;
    5}

    This one line replaces the need to lock <body> scroll in most cases, though you may still want a body lock for iOS Safari versions where background touch events near the edges of the viewport can still trigger elastic scroll on the page itself, a known rough edge on iOS even after adopting overscroll-behavior (Safari's implementation, shipped in Safari 16 in 2022, is generally solid for the containment case but has had inconsistent behavior with position: fixed overlays in earlier point releases).

    Is overscroll-behavior a substitute for scroll locking?

    Mostly, but not entirely. overscroll-behavior: contain stops chaining once the user is scrolling inside the target element. It does not prevent the user from scrolling the background if their pointer or touch starts outside the modal, for instance by touching the backdrop directly. If your backdrop is meant to be fully non-interactive, you still need pointer-events: none on the backdrop's scrollable ancestors, or you need to keep the body-lock pattern as a belt-and-suspenders measure for that specific gesture.

    Also worth knowing: overscroll-behavior operates at the level of the nearest scroll container, not the whole document tree at once. If you have three nested scrollers (page, then a sidebar, then a list inside the sidebar), setting contain on the innermost list only stops chaining from the list to the sidebar. If you want to guarantee the page itself never moves no matter how deep the nesting, you need contain (or none) set on every level you want to stop the leak at, or simply set it high enough in the tree to catch everything below it.

    Browser support and known gaps

    Support is broad as of 2026. Chrome has supported overscroll-behavior since version 63 (released December 2017), Firefox since version 59 (2018), and Safari added support in Safari 16 (2022), which was a notably late addition and is the main reason older "Safari doesn't support this" advice still circulates in outdated blog posts. Check caniuse for the current matrix before relying on it as a hard requirement rather than a progressive enhancement.

    One real gotcha: overscroll-behavior has no effect on an element unless that element is actually a scroll container, meaning it has overflow: auto, scroll, or hidden (with a scrollable overflow) and a bounded height. Applying it to a div with overflow: visible or unconstrained height does nothing, silently. This trips people up constantly when they add the property to a modal wrapper instead of the inner scrollable content div and then conclude the property "doesn't work."

    Another nuance: overscroll-behavior: none and contain both stop chaining, but only none suppresses the native overscroll glow/bounce visual on the element itself. If your QA reports "the bounce is still happening" after you set contain, that's expected: the containment worked, the ancestor didn't move, but the element's own edge effect is still rendering. Switch to none if you want that effect gone too.

    Testing it properly

    Don't test scroll chaining fixes only with a mouse wheel on desktop. Wheel deltas and touch/trackpad momentum scrolling behave differently, and momentum scrolling is exactly the case most likely to expose chaining bugs because the browser keeps applying velocity after the finger lifts. Test with:

    • Real touch on iOS Safari and Android Chrome, specifically flicking fast enough to generate momentum past the content boundary.
    • Trackpad scrolling on macOS in both Chrome and Safari, since macOS trackpad gestures generate continuous deltas that differ from wheel mice.
    • DevTools' touch emulation as a first pass only. It approximates gestures but won't catch iOS-specific elastic scroll edge cases.

    If you're auditing an existing codebase for scroll-lock hacks that can be simplified, search for preventDefault calls inside wheel, touchmove, or scroll event listeners, and for any overflow: hidden toggling on <body> triggered by modal open/close state. Most of those can be replaced or backed up with a single overscroll-behavior: contain declaration, cutting real JavaScript out of your bundle and removing a class of "scroll position jumped when I closed the modal" bugs that body-lock hacks are notorious for introducing. The MDN reference and the CSSWG spec draft are worth reading in full once, since the interaction between contain, none, and nested scroll containers is easy to misremember under pressure during a bug fix.

    Advertisement

    728 × 90 — Leaderboard — Google AdSense

    Related Articles