Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

CSS-Only Carousels with scroll-snap

C
Carlo Fontanos
· 2 min read

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 proximity and 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.

C
Written by Carlo Fontanos

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.

Message sent!

Your details are only used to reply to you.

Keep reading