Web development

Carousels with scroll-snap: three properties and no JavaScript

scroll-snap turns an overflow container into a carousel that snaps, with no JavaScript. The three properties, the edge case and the keyboard check.

Key points

  • Three properties are enough: scroll-snap-type on the container, scroll-snap-align on the child, scroll-padding for the first element.
  • mandatory for cards of equal width, proximity when the cards differ.
  • CSS gives you neither the label nor the counter: those you still write, and they matter.

Short answer: scroll-snap-type plus scroll-snap-align turn a container with overflow-x: auto into a carousel that snaps, with no JavaScript. Dragging stays native and the keyboard keeps working.

The three properties you need

PropertyWhereWhat it does
scroll-snap-type: x mandatorycontainersnaps on the horizontal axis, with no in-between
scroll-snap-align: startchildsays where the element comes to rest
scroll-padding-inlinecontaineraccounts for the padding, so the first element is not clipped

The code

.carousel {
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: 1rem;
  overscroll-behavior-inline: contain;
  scrollbar-width: none;
}

.carousel > * {
  flex: 0 0 min(80vw, 22rem);
  scroll-snap-align: start;
}

@media (prefers-reduced-motion: reduce) {
  .carousel { scroll-behavior: auto; }
}

overscroll-behavior-inline stops the gesture from carrying on into the page when the carousel reaches the end: without it, vertical scrolling jams on a phone. min(80vw, 22rem) gives a nearly full card on a phone and a fixed one on desktop.

mandatory or proximity

mandatory forces the snap and works for cards of equal width. When the cards differ in size the snap feels like a jump: proximity is gentler there, because it only snaps when you are already near the point.

Support has been broad since 2019. On dated Safari you still need -webkit-overflow-scrolling: touch for inertial scrolling, and that property must come before overflow.

The limitation

A scroll-snap carousel has no arrows, no counter and no label. A screen reader user cannot tell how many cards exist: the container needs an accessible label and a text indicator such as “3 of 8”. CSS alone does not give you either, and without them the carousel is an invisible horizontal list.

How to check it

  • Drag with a trackpad, a touch screen and a keyboard: the snap must land on the same element.
  • Check the tab order: it must follow the visual order, not the DOM order if you split them.
  • Turn on prefers-reduced-motion in the system: no animated scrolling.
  • Shrink the window to 320px: the first card must not be clipped by the padding.

Where to continue

In the catalogue: testimonial block, components and blocks. The guides: centring an element and a navbar on mobile; the comparison: Tailwind against CSS Modules.

Frequently asked questions

Do I still need a carousel library?

Not for scrolling. A library earns its place when you want autoplay, infinite loop or arrows with state, which scroll-snap does not do.

Why does the snap not work?

Almost always because the container has no overflow declared, or because the property sits on an ancestor. scroll-snap-type goes on the element that scrolls.

How do I hide the scrollbar?

With <code>scrollbar-width: none</code> and <code>::-webkit-scrollbar</code> for older engines. Remember that hiding the bar also removes the hint that there is more to scroll.

Does it work vertically?

Yes, with <code>scroll-snap-type: y</code>. On a whole page it is invasive though: I would only use it inside a panel with a fixed height.

Does the carousel hurt CLS?

No, if the cards declare a ratio. The risk is width, not height: a card with no measurement changes how many items are visible while it loads.