Recreating the Rubber Band Effect With CSS Scroll Snap
The “rubber band” scrolling effect found on many mobile devices is a familiar one: a page or container lets you scroll slightly past its boundary, then snaps back when you let go. In most browsers on touch devices, this behavior happens automatically. But what if you need to force it in a project, particularly one that must also work on devices without touch input?
JavaScript would be the obvious approach, but it quickly becomes complex with pointerDown, pointerUp, and pointerMove listeners, plus manual tracking of position and inertia. A CSS-only solution is far cleaner—and it doesn’t require relying on non-standard properties like -webkit-overflow-scrolling, which addresses a different type of momentum scrolling anyway.
The Core Setup
Start with a scrollable container holding a few child elements:
<div class="carousel">
<div class="slides">
<div class="slide">1</div>
<div class="slide">2</div>
<div class="slide">3</div>
<div class="slide">4</div>
<div class="slide">5</div>
</div>
</div>
Add baseline styles to guarantee the content will overflow the 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;
}
Applying Margins for the Overscroll Effect
Vertical margins on the child elements create the “stretch” area above and below the content. With a single long child, add margin to its top and bottom. With multiple children, apply the margin to the top of the first child and the bottom of the last child:
.carousel > .slides > .slide:first-child {
margin-top: 100px;
}
.carousel > .slides > .slide:last-child {
margin-bottom: 100px;
}
Now the container can be scrolled past its edges, but there’s nothing yet pulling it back. That’s where scroll-snap-type and scroll-snap-align come in:
.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;
}
With the first child aligned to the start of the scroll container and the last child aligned to its end, the browser will snap back to the edge boundaries. The effect is the CSS equivalent of the native rubber band behavior.
Supporting Horizontal Scrolling
The same technique works for a horizontally scrolling container with a few simple changes. Apply margin to the left and right edges of the children instead of the top and bottom, and change the scroll-snap-type value from y mandatory to x mandatory.
Resources
- “The inside story of the iconic ‘rubber band’ effect that launched the iPhone” (Cult of Mac)
- “Six things I learnt about iOS Safari’s rubber band scrolling” (Special Agent Squeaky)
- “Scroll Bouncing On Your Websites” (Smashing Magazine)



