Elastic Overflow Scrolling | CSS-Tricks
A client asked if we could mimic the “rubber band” scrolling behavior on many mobile devices. I’m sure you know what I’m talking about. It’s a behavior that already exists and happens automatically in most browsers. In iOS Safari, for example, you’re allowed to scroll beyond the top or bottom edge of the viewport by a few hundred pixels, and letting go snaps the page back in place.
I had heard of some instances where someone might want to prevent the bounce from happening but no one had asked me to implement it, especially in a way that supports devices without a touch interface. I was actually a bit surprised there isn’t an existing CSS property for this. There’s the non-standard -webkit-overflow-scrolling
property but that’s for a different type of “momentum” scrolling. Nor would I want to rely on a non-standard property that’s not on track to become part of the specifications.
OK, so what if we want to force this sort of rubber banding in our work? For starters, we’d need some sort of element acting as a container for content that requires scrolling. From there, we could reach for JavaScript, of course, but that involves adding scroll listeners or a combination of pointerDown
, pointerUp
, and pointerMove
events, not to mention keeping track of positions, inertial movement, etc.
A CSS-only solution would be much more ideal.
Here is a container with a few child elements:
Let’s get some baseline styles in place, specifically to create a situation where we’re guaranteed to overflow a parent container.
/* Parent container with fixed dimensions for overflow */
.carousel {
width: 200px;
height: 400px;
overflow-x: hidden;
overflow-y: auto;
}
/* Wrapper for slides, stacked in a column */
.slides {
display: flex;
flex-direction: column;
flex-wrap: wrap;
width: 100%;
height: fit-content;
}
/* Each slide is the full width of the carousel */
.slide {
width: 100%;
aspect-ratio: 1;
}
Let’s start by adding some vertical margins. If your container has only one long item, add it to the top and bottom of the child element. If the container has multiple children, you’ll want to add margin
to the top of the first child element and the bottom of the last child element.
.carousel > .slides > .slide:first-child {
margin-top: 100px;
}
.carousel > .slides > .slide:last-child {
margin-bottom: 100px;
}
Great! We can now scroll past the edges, but we need something to snap it back after the user lifts their finger or pointer. For this, we’ll need the scroll-snap-type
and scroll-snap-align
properties
.carousel {
scroll-snap-type: y mandatory;
}
.carousel > .slides > .slide {
scroll-snap-align: start;
}
.carousel > .slides > .slide:first-child {
margin-top: 100px;
}
.carousel > .slides > .slide:last-child {
scroll-snap-align: end;
margin-bottom: 100px;
}
Note that the same applies to a horizontally scrolling element. For that, you’d change things up so that margin
is applied to the element’s left and right edges instead of its top and bottom edges. You’ll also want to change the scroll-snap-type
property’s value from y mandatory
to x mandatory
while you’re at it.
That’s really it! Here’s the final demo:
I know, I know. This isn’t some Earth-shattering or mind-blowing effect, but it does solve a very specific situation. And if you find yourself in that situation, now you have something in your back pocket to use.
Additional resources