Carousels

CSS scroll-snap with ::scroll-button() and ::scroll-marker()

Limited availability

Features it needs

  • Scroll snapWidely available
  • ::scroll-buttonLimited availability
    • Chrome135
    • Edge135
    • Firefox-
    • Safari-
  • Scroll markersLimited availability

A scroll container with scroll-snap has covered the swiping half of a carousel for years. What was missing was the chrome around it: previous and next buttons, and a row of dots that tracks the current slide. ::scroll-button() and ::scroll-marker() generate both from CSS, and the browser wires up the scrolling, the focus order and the accessible names. The markers are real tab stops, so keyboard and screen reader support comes for free rather than being something you have to add back.

When this applies

Building a horizontal gallery with prev/next buttons and dot indicators.

The native approach

.carousel {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 80%;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  /* Put the dot row after the scroller. */
  scroll-marker-group: after;
}

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

/* Prev / next, generated and wired up by the browser. */
.carousel::scroll-button(inline-start) {
  content: "\2190";
}
.carousel::scroll-button(inline-end) {
  content: "\2192";
}
.carousel::scroll-button(*):disabled {
  opacity: 0.4;
}

/* One dot per slide. */
.carousel > *::scroll-marker {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.3;
}
.carousel > *::scroll-marker:target-current {
  opacity: 1;
}

MDN referenceSee it working, with an explanation

When the dependency is still right

An answer that always says "the platform covers it" is worse than no answer. These are the cases where this one does not hold.

  • You need Safari or Firefox support today. Both ship scroll-snap but not ::scroll-button() or ::scroll-marker(), so they fall back to a bare scroll container with no buttons and no dots.
  • You need autoplay, infinite looping, or a coverflow-style 3D effect. None of these have a CSS equivalent.
  • You render hundreds of slides and rely on the library to virtualise them.
  • Slides animate as a function of scroll position, for example parallax or a scaling centre slide.
  • You need programmatic control from JavaScript beyond scrollIntoView, such as pause on hover or a progress bar tied to a timer.

Building it

This catalog stops at the swap. These guides go through the implementation and the fallbacks. They come from Google Chrome's modern-web-guidance, Apache-2.0.

Packages this covers