CSS-Only Carousels with scroll-snap
Carousel libraries fight the browser: they hijack touch events, reimplement momentum physics, and still feel slightly wrong on every device. scroll-snap takes the opposite approach - the browser keeps doing native scrolling, and CSS just declares where it should come to rest.
A complete swipeable carousel
.carousel {
display: flex;
gap: 16px;
overflow-x: auto;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
}
.carousel > * {
flex: 0 0 85%;
scroll-snap-align: center;
}
<div class="carousel">
<img src="1.webp" alt="...">
<img src="2.webp" alt="...">
<img src="3.webp" alt="...">
</div>
That's a functioning carousel: swipe on touch, scroll-wheel or shift-wheel on desktop, keyboard scrolling for free, and items always settle centered. The 85% width leaves a peek of the next slide, which is better UX than any dot indicator - users see there's more.
The three properties that matter
- scroll-snap-type: x mandatory - the container must always rest on a snap point. Swap mandatory for
proximityand snapping only engages near a snap point, which suits long mixed-content scrollers. - scroll-snap-align - where each child anchors: start, center or end. Product cards: start. Gallery images: center.
- scroll-snap-stop: always - forces a hard stop at every item, preventing a strong flick from skipping slides. Use it for step-by-step onboarding; skip it for casual galleries where flicking through fast feels good.
The companion nobody sets: scroll-padding
When the container has a sticky header or inset edge, snap positions can land under it. scroll-padding on the container offsets every snap point:
.carousel { scroll-padding-inline-start: 24px; }
Its close cousin scroll-margin works per-child, and also fixes anchor links hiding under fixed headers - same mechanism, different axis.
Where JavaScript still earns its place
Autoplay, infinite looping and synced dot indicators need script. But note the shape of that script: read scrollLeft, call scrollTo() - cooperating with native scrolling rather than replacing it. For indicators, an IntersectionObserver per slide with a 60% threshold tells you which slide is active in about six lines.
Support has been solid across the board since 2020. Every carousel I've shipped since is scroll-snap underneath, and the bug reports about weird swipe behavior simply stopped.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.