How to build a swipeable carousel with just CSS

How to build a swipeable carousel with just CSS

Creem.io

Merchant of record payments for SaaS and digital products, handling checkout, subscriptions, taxes, invoices, and global payment operations.

Get started today

I’m sure you’ve been there – you need a simple carousel: testimonials, product cards, a photo strip. So you reach for a library, ship 30kb of JavaScript, and spend an hour fighting its options. Right?

But the browser already knows how to scroll sideways and stop at a right position.

This feature is called CSS scroll snap, and it turns a plain overflow container into a swipeable carousel.

The fix

html
<ul class="carousel">
  <li><img src="photo-1.jpg" alt="..." /></li>
  <li><img src="photo-2.jpg" alt="..." /></li>
  <li><img src="photo-3.jpg" alt="..." /></li>
</ul>
css
.carousel {
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}

.carousel > li {
  flex: 0 0 80%;
  scroll-snap-align: start;
}

And that’s the whole carousel. Swipe on touch, two-finger scroll on a trackpad, and every gesture lands exactly on a slide.

Let’s see what we have now.

Where this comes up

  • Testimonial sliders: the classic “what our customers say” row.
  • Product galleries: image strips on shop pages, where every kb of JS hurts conversion.
  • Card rows on mobile: pricing plans or features that sit in a grid on desktop but should swipe on small screens.

The values, explained

scroll-snap-type: x mandatory means the container always rests on a snap point. Swap mandatory for proximity if you only want snapping when the user stops near a slide:

css
.carousel {
  scroll-snap-type: x proximity;
}

scroll-snap-align controls where each slide parks. Use center for a peek of the neighbors on both sides:

css
.carousel > li {
  scroll-snap-align: center;
}

Two useful extras. scroll-snap-stop: always prevents a hard flick from skipping past slides:

css
.carousel > li {
  scroll-snap-stop: always;
}

And scroll-padding keeps the first slide from hiding under any inset or edge fade:

css
.carousel {
  scroll-padding-inline: 1rem;
}

One caveat worth testing in your own layout: Chrome makes a scroll container keyboard focusable only when it has no focusable children. The moment your slides contain a link or a button, which real testimonial and product cards do, that stops applying and arrow keys no longer reach the carousel. Add tabindex="0" to the container when your slides are interactive:

html
<ul class="carousel" tabindex="0">

Why this matters

  • Zero JavaScript: nothing to load, nothing to break, nothing to maintain.
  • Native gestures: touch, trackpad, and mouse wheel all work because it’s real scrolling, not a simulation of it.
  • Semantic markup: screen readers see a plain list, not a pile of divs with ARIA bolted on.
  • Broad support: scroll-snap-type and scroll-snap-align work in every modern browser. scroll-snap-stop is newer, so treat it as an enhancement rather than something you rely on.

Open a project with a carousel library in package.json. Find one carousel that’s mostly used on mobile, swap it for the six lines above, and see what you actually lose.

Happy coding!

Marko

Rate this issue

Your ratings shape which issues I recommend to you.