CSS Overscroll-Behavior Table
| Piece | What it does | Field note |
|---|---|---|
overscroll-behavior: contain | Stop scroll chaining | Inner scroll ending no longer scrolls the PAGE - the modal fix |
overscroll-behavior: none | Contain + no glow | contain plus no bounce/rubber-band on the boundary |
Scroll chaining | The default handoff | Inner scroll ends โ outer scrolls - sometimes exactly wrong |
Modal/dialog scrolling | The classic case | Body scrolls behind an open modal - contain on the modal |
Pull-to-refresh region | The trade-off | none disables the browser PTRA - apps lose it accidentally |
scroll-boundary vs scroll-empty | When contain bites | Only at the EDGE - normal scrolling inside is untouched |
touch-action sibling | Gesture-level control | touch-action controls gestures; overscroll controls chaining |
Infinite-scroll pairs | Outer still scrolls | Inner chat + outer feed: contain the chat, keep the feed |
Scroll chaining is the default handoff: when an inner scroller reaches its end, the OUTER page starts scrolling - sometimes exactly wrong (a chat log ends and the whole page lurches under the user's thumb). overscroll-behavior: contain stops the chaining at the edge only.
Bottom line: contain is the modal fix (the body stops scrolling behind an open dialog), the chat-pane fix, and the embedded-scroller fix - normal scrolling inside is untouched, only the edge handoff is suppressed. none adds suppression of the boundary bounce, and accidentally disables the browser's pull-to-refresh - the trade teams discover in review.
The honest part: contain bites only at the scroll BOUNDARY (scroll-boundary vs scroll-empty) - it is not a general scroll-blocker, and the gesture-level sibling is touch-action, which controls what gestures do rather than where leftover scroll goes.
How to use
- Fix the modal: .modal-content { overscroll-behavior: contain; } - scrolling to the modal's end stops there; the page behind stays put.
- Contain the chat: message panes and embedded feeds get contain - reading to the end of history no longer scrolls the surrounding page.
- Know the PTRA trade: overscroll-behavior: none on the body disables pull-to-refresh on Android - decide deliberately, not by copy-paste.
Frequently asked questions
What is scroll chaining and when is it the wrong default?
The inherited scroll. When a gesture outlives the inner scroller's content - you reach the bottom of a chat pane and keep dragging - the leftover scroll transfers to the next ancestor that can scroll, ultimately the page itself. The default makes small scrollers feel connected to the page. It is exactly wrong whenever the inner scroller is a FOCUS SURFACE: a chat pane whose end should just stop, a code block being read line-by-line, a carousel region - the page lurching under a focused interaction reads as broken. contain localizes the gesture's consequences to the scroller the user is actually touching.
What is the difference between contain and none - and why does none carry a trap?
Scope of suppression. contain stops the CHAINING (edge handoff to ancestors) but keeps the local boundary behavior - rubber-banding inside the scroller stays. none does that AND suppresses the element's own boundary effects - which on Android includes the browser-level PULL-TO-REFRESH when applied to the root scrolling element. The trap is accident-shaped: a team copies none from a modal fix onto body or html, and the site silently loses pull-to-refresh for every mobile user - a discovery made in review or not at all. The decision rule: contain for embedded scrollers (the targeted fix), none only when the boundary effects themselves are the problem, and never on the root without a deliberate product decision about PTRA.
How does this fix the body-scrolls-behind-modal problem?
The classic complaint, solved in one line. When a modal opens, scrolling inside it works until its content ends - then the page BEHIND starts scrolling (chaining), moving the page out from under the dialog. The old fixes were JS: preventDefault on wheel with state tracking, or position: fixed body locks with scrollbar-compensation hacks. overscroll-behavior: contain on the modal's scrollable region is the CSS fix: the modal's scroller simply ends, and the gesture dies there - the page behind never receives the leftover scroll. No listeners, no body locking, no layout shift from the disappearing scrollbar. The one case it does not cover: modals whose content NEVER scrolls - there, the touch starts on non-scrollable modal surface and chaining is immediate; pair with a page-level lock for that shape.
How does overscroll-behavior relate to touch-action?
Different layers of the same gesture stack. touch-action (set on the element) controls which GESTURES the browser handles at all: pan-x, pan-y, pinch-zoom - it can disable scrolling before it starts, and it is how carousels claim horizontal swipes. overscroll-behavior controls what happens to scroll that already happened when it EXHAUSTS: chaining and boundary effects. They cooperate in the infinite-scroll pair: an inner chat with overscroll-behavior: contain keeps its edge contained while the outer feed scrolls normally, and touch-action on inner interactive elements stops gestures from being misread. The debugging split: 'the wrong thing scrolls' is overscroll; 'the thing won't scroll or zoom' is touch-action.