Using CSS Overscroll-Behavior To Prevent Scrolling Of Parent Containers From Within Overflow Containers
In the past, I've looked at how the scroll-wheel seems to randomly stop working in an overflow container. This phenomena is related to a browser feature called scroll chaining; and, it can be overcome if you prevent the wheel
event's default behavior. Of course, tapping into the wheel
and scroll
events is not great for browser performance. Luckily, Derek Duncan stepped-in and told me about a CSS property called, overscroll-behavior
. Supported in Chrome, Firefox, and Edge, this CSS property allows us to declaratively control what happens when an overflow container hits the edge of its scrollable content. This is a game changer!
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
If you have an overflow: auto
element, the user can scroll the content contained within that element. However, when the user hits the top or the bottom of that content, the browser may start to scroll one of the ancestor elements, most commonly the body
element. Once this "scroll chaining" occurs, subsequent use of the mouse-wheel may not be applied to the overflow: auto
element; instead, the expression of the scrolling may continue to manifest in the ancestor element.
More often than not, this leads to an unexpected and undesired user experience (UX). Really, what we want to happen is to have the scrolling behavior always contained within the overflow: auto
element. To do this (in Chrome, Firefox, and Edge), we can add the CSS property overscroll-behavior: contain
to the overflow: auto
element. This will prevent the "scroll chaining" behavior, which will, in turn, keep the mouse-wheel active within the target element.
To see this in action, I have two overflow: auto
elements laid-over a scrollable body
element. The container on the left has no modifying CSS properties while the container on the right has the CSS property overscroll-behavior: contain
:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>
Using CSS Overscroll-Behavior To Prevent Scrolling Of Parent Containers From Within Overflow Containers
</title>
<style type="text/css">
html {
box-sizing: border-box ;
}
*, *:before, *:after {
box-sizing: inherit ;
}
.layout {
background-color: #ffffff ;
border: 5px solid #cccccc ;
height: 500px ;
margin: -250px 0px 0px 0px ;
position: fixed ;
top: 50% ;
width: 300px ;
}
.layout--a {
right: 51% ;
}
.layout--b {
left: 51% ;
}
.layout__top-panel {
bottom: 100px ;
left: 0px ;
overflow: auto ; /* The panel becomes SCROLLABLE due to content overflow. */
position: absolute ;
right: 0px ;
top: 0px ;
}
.layout--b .layout__top-panel {
overscroll-behavior: contain ; /* Prevent SCROLL-CHAINING to parent elements. */
}
.layout__bottom-panel {
border-top: 1px solid #cccccc ;
bottom: 0px ;
font-weight: bold ;
height: 100px ;
left: 0px ;
padding: 20px 20px 20px 20px ;
position: absolute ;
right: 0px ;
}
.content p {
margin: 0px 0px 0px 0px ;
padding: 25px 20px 22px 20px ;
}
.content p:nth-child( even ) {
background-color: #f0f0f0 ;
}
</style>
</head>
<body>
<h1>
Using CSS Overscroll-Behavior To Prevent Scrolling Of Parent Containers From Within Overflow Containers
</h1>
<!-- BEGIN: Layout-A. -->
<section class="layout layout--a">
<div class="layout__top-panel">
<div class="content">
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
</div>
</div>
<div class="layout__bottom-panel">
No Behavior Modification
</div>
</section>
<!-- END: Layout-A. -->
<!-- BEGIN: Layout-B. -->
<section class="layout layout--b">
<div class="layout__top-panel">
<div class="content">
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
</div>
</div>
<div class="layout__bottom-panel">
Using Overscroll-Behavior
</div>
</section>
<!-- END: Layout-B. -->
<!-- BODY content. -->
<div class="content">
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
<p>Content</p><p>Content</p><p>Content</p><p>Content</p><p>Content</p>
</div>
</body>
</html>
Notice that the only difference is that the overflow: auto
container on the right has the overscroll-behavior
directive. Now, if we run this HTML and CSS code in the Chrome browser, we get the following output:
As you can see, when users scrolls to a local maxima within the scrollable container on the left, subsequent use of the scroll wheel causes the body
element to scroll. This is "scroll chaining" in action. However, with the scrollable container on the right, which uses overscroll-behavior: contain
, the scrolling is contained within the element - no "scroll chaining" occurs and the mouse wheel never affects the body
scroll.
This is awesome! I will literally be shipping this to production today. The overscroll-behavior
CSS property is perfect for things like rich HTML dropdown menus, modal windows, and fly-out panels. A true game-changer for creating a more positive user experience (UX) in my Angular, Single-Page Applications (SPA). Much thanks to Derek Duncan for learning me some new CSS hawtness!
Want to use code from this post? Check out the license.
Reader Comments
Awesome! I can use this TODAY! Love it when something like this falls right into my lap, solving a problem I didn't realize had a solution!
@Chris,
1000% :D I already deployed a change yesterday with this update. Loving it.
This is great! Unfortunately does not work on Safari, which is too bad since I need it for iOS :(
Will circle back in the future if I ever run into the need again though.
@Ted,
That said, the beauty of CSS like this is that you can use it a "progressive enhancement" feature. Meaning, you can put it in, and then users who have browsers that support it will get a slightly better experience; and, users who have browsers that do not support it, will just get the "usual" experience. So, it's basically just a value-add.
I would also mention, though, that mobile Safari uses a very different scrolling behavior for reasons that relate to small-screen usability. It might not be worth it to try an override it.
A super useful property, many thanks. Googled it.
@Glib,
Awesome, glad you found this helpful. I'm using this CSS property all the time now :D
Thanks !!!
Thank you so much, Ben! I started to have depression because of this bug ;)
@Dmitriy,
Ha ha, glad this was helpful. I use this CSS property all the time these days. It's a wonderful progressive enhancement.
Very cool. Is there a way to scroll the outer box first? Also allow me to specify scroll direction, something like "scroll-down-outer-first"
I've got a feature request recently, asking to scroll the outer container first if scroll direction is downward.
Never done this before, in the end, I used onwhell event to pull this off: https://realrz.com/pub/video/scroll_outer_first.mov
This was a big help, and still relatively unknown. I stumbled upon this early, and i'm glad I did!