Scroll chaining, and the one CSS line that refuses it.
When a scrollable element reaches its end, the browser does not stop. It hands the remaining scroll to the nearest scrollable ancestor. That handoff is called scroll chaining, and it is the default — nobody switched it on. overscroll-behavior is how you refuse it.
The default handoff. Inner box hits its bottom, leftover scroll travels up to the nearest scrollable ancestor — usually the page behind your modal.
containStops the handoff at this element's boundary. The page behind stays put. The native overscroll affordance (bounce, pull-to-refresh) still works.
nonecontain plus suppression of the affordance itself — no rubber-band bounce, no pull-to-refresh gesture.
Both values are per-axis. overscroll-behavior-y is the common case: a drawer or chat panel that scrolls vertically.
| Value | Chaining | Overscroll affordance |
|---|---|---|
auto | Yes — the default | Kept |
contain | Blocked at the boundary | Kept |
none | Blocked at the boundary | Suppressed |
overscroll-behavior-y: contain: propagation stops at the boundary. The leftover delta is discarded and the page holds still.The old fix was a scroll or touchmove listener calling preventDefault(). That fights the compositor and drops frames, because it runs on the main thread — the browser must wait to hear whether you'll cancel before it can scroll. overscroll-behavior is declarative and handled where scrolling already lives, off the main thread.
none silently kills a gesture the OS provides and you cannot re-implement.1. none removes pull-to-refresh. On mobile that is a gesture users expect; suppressing it on a full-page container breaks an affordance you did not build and cannot replace. Reach for contain unless you specifically want the bounce gone.
2. contain only stops chaining at a boundary. It does not make an element scrollable. If the inner element has no overflow, there is no boundary and nothing to contain — which is exactly why the property so often appears to do nothing at all.
Scroll to the exact bottom of the inner box, then keep dragging. With chaining, the page moves. With contain, it does not. If nothing changes either way, your inner element probably isn't scrolling in the first place — check that it actually overflows.
overscroll-behavior: contain on a modal and reports no change at all. What is the first thing you check, and why is the property innocent?
Hint: containment needs a boundary to contain.contain and none in one sentence, then name the concrete cost of choosing none on a full-page container.
Hint: one blocks a handoff; the other also blocks a gesture.preventDefault() scroll listener worse than this CSS property even when both produce the same visible result?
Hint: think about which thread each one runs on, and what the browser must wait for.