A scrollable panel inside a modal keeps scrolling the page behind it once it reaches its own end. Which CSS property stops that, and how do its values differ?
answer
- the scroll leaks upward, not sideways
- containment versus affordance
- two values both stop the leak
- it needs something to scroll first
- modal lock needs a second half
basics
~10 soverscroll-behavior controls scroll chaining. Setting overscroll-behavior: contain on the panel keeps the scroll inside it once it hits an edge; none does the same and additionally suppresses the platform's overscroll effect such as pull-to-refresh.
solid answer
~50 sThe behaviour is called scroll chaining: when a scroll container reaches its end, the browser passes the remaining scroll to the nearest scrolling ancestor, and eventually to the page. `overscroll-behavior` — with per-axis longhands `overscroll-behavior-x`/`-y` and the logical `-inline`/`-block` — controls it. `auto` is the default chaining behaviour. `contain` stops the chain at that element while keeping the platform's local overscroll affordance, such as the rubber-band glow. `none` stops chaining *and* suppresses the affordance, which is how you disable pull-to-refresh when you set it on the page's own scroll container. For a modal, `overscroll-behavior: contain` on the scrollable panel is the right declaration. Note the limit: the property only applies to scroll containers, so if the modal body has nothing to scroll the touch gesture still moves the page and you need a different scroll-lock. It is supported in all major engines, Safari since version 16 in 2022.
code
css · 5 lines.drawer__body {
max-block-size: 70vh;
overflow-y: auto;
overscroll-behavior-y: contain;
}go deeper
Know the property name and that overscroll-behavior: contain on a scrollable overlay keeps its scrolling from spilling into the page behind it.
Explain scroll chaining and the difference between contain and none — both stop the chain, only none also suppresses the platform overscroll effect such as pull-to-refresh.
Show that you have shipped this: the property only works on a real scroll container, so a complete overlay also needs a page-level scroll lock with the page's scroll offset preserved and restored.
Own the overlay contract for the codebase — one overlay primitive that always contains its scroll and locks the page consistently, rather than each feature reinventing a scroll lock with its own regressions.
## What scroll chaining is When you scroll inside a nested scroll container and it runs out of scrollable area, browsers do not stop — they hand the leftover delta to the next scroll container up the ancestor chain, and ultimately to the viewport. That handoff is *scroll chaining*, and it is the correct default for ordinary nested content: a user flicking through a page should not get stuck because their pointer happened to be over a small scrollable box. It is exactly wrong for overlays. A dialog, drawer, or dropdown is conceptually a separate surface floating above the page. When the user scrolls to the bottom of its content and the page behind starts moving, the illusion breaks and the page's scroll position is lost — a bug users experience as "the background jumped". ## The property ```css .modal-body { overflow-y: auto; overscroll-behavior: contain; } ``` `overscroll-behavior` is a shorthand for `overscroll-behavior-x` and `overscroll-behavior-y`; logical longhands `overscroll-behavior-inline` and `overscroll-behavior-block` also exist. Its values: - **`auto`** — the initial value. Scrolls chain normally to ancestors. - **`contain`** — no chaining past this element. The local overscroll affordance is preserved: on touch platforms the user still gets the rubber-band or glow that says "you have reached the end". - **`none`** — no chaining, and the local overscroll affordance is suppressed as well. Nothing bounces, nothing glows. For an overlay, `contain` is nearly always the right choice: it fixes the leak while keeping the feedback that tells the user the surface has ended. `none` is for cases where the affordance itself is the problem — most commonly disabling pull-to-refresh on a page whose top area has its own gesture, by setting `overscroll-behavior-y: none` on the page's own scroll container. ## The trap: it only applies to scroll containers The declaration does nothing on a box that is not a scroll container. A modal whose body happens to be short — nothing to scroll — will still let a touch drag or wheel gesture move the page behind it, because there is no local scroll for the chain to start from. This is why `overscroll-behavior` alone is not a complete modal scroll lock; it fixes the *chaining* case, not the *no local scroll* case. A robust overlay therefore usually combines two things: `overscroll-behavior: contain` on the scrollable region, and a page-level scroll lock while the overlay is open. The usual page-level lock is `overflow: hidden` on the page's own scroll container — noting that the page then still holds its scroll offset, so the position must be preserved and restored rather than left to chance. Where the platform supports a top-layer overlay mechanism, some of this is handled for you and less manual locking is required. ## Related but different It is worth separating three things people conflate: - **`overscroll-behavior`** — where a scroll goes when a container reaches its end. A layering rule. - **`overflow`** — whether a box clips and scrolls at all. A containment rule. - **`scroll-behavior: smooth`** — how a *programmatic* or fragment-triggered scroll animates. Nothing to do with chaining, and pairing it with `prefers-reduced-motion` is the accessible practice. ## Applying it well - Put `overscroll-behavior: contain` on every overlay scroll region as a matter of course; the cost is nil and it removes a whole class of bug reports. - Prefer `contain` to `none` unless you have a specific reason to remove overscroll feedback — the feedback is a real usability signal on touch devices. - Be axis-specific when only one direction is a problem: `overscroll-behavior-y: contain` on a vertically scrolling drawer leaves horizontal behaviour untouched. - Remember it is only meaningful on a scroll container. Applying it to a static wrapper is a no-op that reads like a fix in review and is not one. - Long lists inside overlays benefit twice: chaining containment plus the fact that the overlay's own scroll no longer competes with the page's. ## Support Chrome and Firefox shipped `overscroll-behavior` years ago; Safari added it in version 16 in 2022, so it is available across all major engines today. Older code often works around its absence with script that cancels the gesture, which is worth deleting when you find it.
- When would you choose none over contain?When the platform's overscroll affordance is itself unwanted. The common case is disabling pull-to-refresh: `overscroll-behavior-y: none` on the page's own scroll container stops the chain and suppresses the gesture, which matters for an app whose top region has a custom pull interaction. For overlays, `contain` is preferred because the rubber-band feedback usefully signals that the surface has ended.
- Why does overscroll-behavior: contain sometimes appear to do nothing on a modal?Because the element it is on is not a scroll container. If the modal body's content fits, there is no local scroll to contain, so a wheel or touch gesture goes straight to the page. The property governs where a scroll chains *from*, not whether the page may scroll at all — which is why overlays also need a page-level scroll lock while they are open.
- How does overscroll-behavior differ from scroll-behavior?They solve unrelated problems. `overscroll-behavior` decides whether a scroll that reaches a container's end chains to an ancestor. `scroll-behavior: smooth` decides whether a programmatic or fragment-triggered scroll animates rather than jumping. One is about propagation between containers, the other about the animation of a single scroll, and setting one never affects the other.
saying these in an interview costs you the question
- Thinks overscroll-behavior prevents all page scrolling
- Applies it to a non-scrolling wrapper and calls it fixed
- Uses none everywhere and kills useful overscroll feedback
- Confuses it with scroll-behavior: smooth
- Believes it needs a script-based gesture workaround today