Carousels

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

Limited availability
  • Chrome135
  • Edge135
  • Firefox-
  • Safari-

Features it needs

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, and each one reads in the terminal through its own command.

Signs it was hand-rolled

No package is involved in any of these, so nothing would match in a package.json. If the code looks like one of them, this rule applies anyway, and the conditions above still decide.

  • a scroll listener on a horizontal strip dividing scrollLeft by item width to work out the active index
  • prev and next buttons calling scrollBy, each with its own disabled state recomputed on every scroll
  • rendering dots from the item count and toggling an active class on whichever one is current

Packages this covers