overscroll-behavior: Controlling Modal Scroll and Scroll Chaining
AI generated
{ }
@
CSS · Scroll · Mobile · UX
overscroll-behavior
Stop scroll chaining in nested containers on purpose, with no JavaScript at all

When a user scrolls inside a modal or dropdown to its edge, the browser by default simply keeps scrolling the page behind it, an effect called scroll chaining. overscroll-behavior deliberately interrupts that handoff at a container's boundary and also prevents accidentally triggering pull-to-refresh on mobile devices, entirely without preventDefault workarounds in JavaScript.

13 min read overscroll-behavior · contain · none Chrome · Firefox · Safari · Mobile

1. What scroll chaining is and why it becomes a UX problem

Scroll chaining refers to the browser default of handing a scroll gesture off to the next ancestor scrollable container once the currently focused container reaches the end of its scroll range. If a user scrolls down through a long product description inside a modal and reaches its bottom, without a countermeasure the page behind it suddenly takes over the remaining scroll motion, and the background jumps further while the user actually still meant to interact with the modal.

This behavior was originally meant as a convenience, for example so a short nested list would not trap the user in a scroll dead end. But for modals, dropdowns, off canvas menus, or image sliders, that same handoff reads as a loss of control: the background moves unexpectedly while the user assumes they are still interacting with the foreground element.

2. The values of overscroll-behavior: auto, contain and none

overscroll-behavior has three values that control behavior at a container's scroll boundary. auto is the default and matches classic scroll chaining, where the motion is handed off unhindered to the next container. contain stops that handoff to ancestor containers but still allows visual overscroll effects like elastic bounce back (rubber banding) at the end of its own container. none goes a step further and suppresses both the handoff and those native overscroll effects entirely.

For most UI containers like modals or dropdowns, contain is the right value, because it ends the disruptive scroll chaining while still leaving the container itself a native, physically feeling scroll behavior at its edge. none fits more specific cases where even the rubber banding feels disruptive, for example a canvas based image viewer with its own zoom logic.


/* Stop scroll chaining inside the modal body,
   rubber banding at its own edge is kept */
.modal-body {
  overflow-y: auto;
  overscroll-behavior: contain;
  max-height: 70vh;
}

3. Controlling individual axes: overscroll-behavior-x and -y

Alongside the combined overscroll-behavior shorthand, overscroll-behavior-x and overscroll-behavior-y allow separate control per axis. That matters especially for horizontally scrolling components like an image slider or a product carousel strip, where only the horizontal handoff should be suppressed while vertical scrolling still passes through to the page normally, for example when the slider doesn't take up the full screen height.

Separate control also avoids a common misunderstanding: applying overscroll-behavior: contain to both axes indiscriminately, even though the container only scrolls horizontally, unnecessarily changes vertical behavior too, which can produce unexpected effects on a container that happens to also overflow slightly in the vertical direction.


/* Only stop horizontal scroll chaining in the slider */
.product-carousel {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

4. Preventing pull-to-refresh on mobile devices on purpose

On mobile browsers like Chrome for Android, pulling down at the top of the page triggers a pull-to-refresh action by default that reloads the entire page. That becomes a problem once a custom UI component, for example a sheet that can be dragged down from the top, or a pull-to-refresh implementation inside a single page application, needs the same gesture for its own purpose, because without a countermeasure the browser interprets the gesture first and accidentally reloads the whole page.

overscroll-behavior-y: contain on the html or body element suppresses exactly that browser level pull-to-refresh once the gesture starts inside a container that is itself scrollable, with no touchstart/touchmove listener with manual preventDefault() needed in JavaScript. That not only cuts code but also avoids the well known performance downsides of non-passive touch event listeners during scrolling.


/* Prevent the browser's own pull-to-refresh on the whole page */
html, body {
  overscroll-behavior-y: contain;
}

5. contain vs. none: when which value is the better choice

The difference between contain and none only concerns the native, physically feeling behavior at the end of the container itself, not the handoff to ancestor elements, which both values suppress equally. contain allows that bounce back and therefore feels familiar on most platforms, because it matches the native scroll feel users already know from other apps.

none suppresses that bounce back completely and fits cases where a custom built scroll or drag behavior would collide with the native rubber banding, for example a self implemented, touch gesture driven bottom sheet that brings its own physics calculations for the spring behavior. For the vast majority of standard UI cases like modals, dropdowns and off canvas menus, though, contain remains the more recommendable, more natural choice.

A scrollable dropdown menu, for example a long country code list in a checkout form, shows the problem particularly clearly: if a user scrolls to the last entry of the list and keeps scrolling with the same gesture, without a countermeasure the form page behind it jumps along too, even though the user was clearly still interacting with the dropdown list. That feels especially disruptive in a sensitive form context like checkout, because an unexpected jump breaks the user's concentration.

With overscroll-behavior: contain set directly on the scrollable dropdown container, the scroll motion ends cleanly at the last list entry, with no reaction from the form page behind it. This single line of CSS reliably replaces older solutions that relied on event.stopPropagation() or manual preventDefault() on wheel and touch events, which regularly missed edge cases like trackpad gestures.


/* Dropdown scrolls independently, without dragging the form page along */
.dropdown-list {
  max-height: 240px;
  overflow-y: auto;
  overscroll-behavior: contain;
}

7. Comparison to the JavaScript approach: why CSS is the better choice here

Before overscroll-behavior, scroll chaining could only be stopped through fairly involved JavaScript logic, usually a wheel or touchmove listener that checks the container's scroll position and calls event.preventDefault() once the boundary is reached. That approach works, but comes with several downsides: non-passive event listeners block rendering during scrolling, the logic has to handle mouse, trackpad and touch input separately, and edge cases like diagonal scrolling are easy to miss.

overscroll-behavior solves the same problem declaratively, right in CSS, with no event listener at all, no rendering blocking JavaScript execution during scrolling, and no platform specific special casing, because the browser already normalizes the input method internally. For new projects there is practically no reason left to suppress scroll chaining through JavaScript once the target browsers support overscroll-behavior.

8. Browser support and a risk free fallback

Chrome, Edge and Firefox have supported overscroll-behavior reliably for several years, Safari caught up with a more recent version, so the property arrives in every relevant browser engine today. A browser that does not know the property simply ignores the declaration and keeps classic scroll chaining, exactly the behavior that already applied before the property was introduced, with no errors or visible break.

Treating this property as a pure enhancement rather than a requirement makes adoption in production projects risk free: there is no need to wait for a feature flag or a JavaScript detection before applying overscroll-behavior: contain across every relevant container. Users on a non-supporting browser simply notice no difference from the previous behavior.

9. Practical use in a shop: cart drawers, filter sheets and image galleries

In a Hyvä context, overscroll-behavior: contain pays off on every scrollable Alpine.js driven container with its own overflow, most notably the cart drawer, the mobile filter sheet in category navigation, and the image gallery on the product detail page. In all three cases, the property prevents a scroll gesture inside the component from accidentally triggering the background page, or on mobile devices, a pull-to-refresh.

Combined with scrollbar-gutter from a related article, the picture becomes complete: overscroll-behavior prevents unwanted scroll handoff between containers, while scrollbar-gutter prevents layout jumps caused by appearing scrollbars. Both properties together make scrollable UI components behave predictably, with not a single byte of JavaScript required.

Value Handoff to ancestors Rubber banding at own edge Typical use
auto (default) Yes, classic scroll chaining Yes No special behavior wanted
contain No, stopped Yes Modals, dropdowns, off canvas menus
none No, stopped No, suppressed Custom scroll physics, canvas viewers
overscroll-behavior-y: contain on html No for the vertical axis Yes Preventing pull-to-refresh on purpose

Mironsoft

Modern CSS, layout architecture and rendering performance

CSS that stays maintainable instead of breaking with every change?

We review existing stylesheets for specificity chaos and layout thrashing, then build a CSS architecture with cascade layers, custom properties and modern layout primitives that still makes sense after the tenth feature.

CSS Audit

Systematically uncovering specificity issues, cascade conflicts and unused selectors.

Architecture Refactoring

Introducing cascade layers, custom properties and design tokens cleanly.

Performance Tuning

Fixing layout thrashing, expensive selectors and rendering bottlenecks.

10. Summary

overscroll-behavior: The Essentials at a Glance

Core idea

overscroll-behavior stops scroll chaining at a container's boundary, so a background page no longer scrolls along unexpectedly.

contain vs. none

contain keeps native rubber banding at its own edge, none additionally suppresses it, for custom built scroll physics.

Pull-to-refresh

overscroll-behavior-y: contain on html or body prevents browser side pull-to-refresh, with no preventDefault in JavaScript.

Separate axis control

overscroll-behavior-x and -y allow separate control, for example for horizontally scrolling sliders without changing vertical behavior.

11. FAQ: overscroll-behavior: The Essentials at a Glance

1What exactly is scroll chaining?
Scroll chaining refers to the default behavior of handing a scroll gesture off to the next ancestor scrollable container once the current container reaches the end of its scroll range.
2What is the difference between contain and none?
Both stop the handoff to ancestor containers. contain still allows native rubber banding at its own edge, none suppresses that effect completely as well.
3How do I prevent pull-to-refresh on mobile devices?
overscroll-behavior-y: contain on the html or body element suppresses the browser's pull-to-refresh once the gesture starts inside a scrollable container, with no JavaScript at all.
4Do I still need JavaScript for scroll chaining control?
In most cases no. overscroll-behavior solves the problem declaratively in CSS and replaces older solutions using wheel or touchmove listeners with manual preventDefault.
5Can I control just one axis?
Yes, overscroll-behavior-x and overscroll-behavior-y let you set horizontal and vertical behavior separately, which matters especially for horizontally scrolling sliders.
6Where should I use overscroll-behavior: contain?
On every scrollable UI container with its own overflow, typically modals, dropdowns, off canvas menus, filter sheets and cart drawers.
7Does overscroll-behavior affect normal scrolling inside the container?
No. Scrolling inside the container itself stays completely normal, the property only takes effect at the edge, once there is no more scroll distance left inside the container.
8Does overscroll-behavior work in all current browsers?
Chrome, Edge and Firefox have supported the property reliably for a while, Safari caught up. Older browsers simply fall back to classic scroll chaining as a harmless default.
9Why does none sometimes feel unnatural?
Because none also suppresses the rubber banding users know from native apps as familiar physical feedback. Without custom scroll physics as a replacement, the container can feel abrupt or stiff as a result.
10Does overscroll-behavior replace the need for scrollbar-gutter?
No, they solve different problems. overscroll-behavior controls the handoff of scroll motion, scrollbar-gutter prevents layout jumps caused by appearing scrollbars. Together they produce stable scroll behavior.