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.
Table of Contents
- 1. What scroll chaining is and why it becomes a UX problem
- 2. The values of overscroll-behavior: auto, contain and none
- 3. Controlling individual axes: overscroll-behavior-x and -y
- 4. Preventing pull-to-refresh on mobile devices on purpose
- 5. contain vs. none: when which value is the better choice
- 6. Practical example: a scrollable dropdown without a background jump
- 7. Comparison to the JavaScript approach: why CSS is the better choice here
- 8. Browser support and a risk free fallback
- 9. Practical use in a shop: cart drawers, filter sheets and image galleries
- 10. Summary
- 11. FAQ
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.
6. Practical example: a scrollable dropdown without a background jump
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.