Entry 01

Carousel on native scroll

Use it for any horizontal row of cards. The carousel is a real scroll container, so swipe, trackpad, keyboard and momentum all work before any script loads.

Open
Swipe, drag the trackpad, and press the arrows. Each stop lands a card on the left padding, and the arrows turn off at both ends.
Code CSS · HTML · JS · 3 blocks · 41 lines
.carousel {
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: 1.5rem;
  padding-inline: 1.5rem;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}

.carousel > * {
  flex: none;
  scroll-snap-align: start;
}

@media (prefers-reduced-motion: no-preference) {
  .carousel { scroll-behavior: smooth; }
}

Arrow buttons are the one part that needs a script.

<button data-carousel-dir="-1" aria-label="Previous">←</button>
<button data-carousel-dir="1" aria-label="Next">→</button>
const carousel = document.querySelector(".carousel");
const [prev, next] = document.querySelectorAll("[data-carousel-dir]");
const sign = getComputedStyle(carousel).direction === "rtl" ? -1 : 1;

const step = (dir) => {
  const card = carousel.firstElementChild.getBoundingClientRect().width;
  carousel.scrollBy({ left: sign * dir * card });
};

const sync = () => {
  const max = carousel.scrollWidth - carousel.clientWidth;
  const scrolled = Math.abs(carousel.scrollLeft);
  prev.disabled = scrolled <= 1;
  next.disabled = scrolled >= max - 1;
};

prev.addEventListener("click", () => step(-1));
next.addEventListener("click", () => step(1));
carousel.addEventListener("scroll", sync, { passive: true });
sync();
Why it works, the rules and the credits 5 reasons · 3 rules · credits

Why it works

  1. Scroll snap makes the browser do the scrolling and the landing. Every input method gets the same result.
  2. scrollBy moves one card width and snapping picks the final position. The script never calculates an offset.
  3. In a right-to-left row scrollLeft starts at 0 and goes negative. sign and Math.abs() make the script work in both directions.
  4. scroll-behavior: smooth in CSS animates the script's scroll. The media query turns the animation off for people who ask for reduced motion.
  5. overscroll-behavior-x: contain stops a swipe at the end of the row from triggering the browser's back gesture.

Rules

  1. Never move slides with transform, and never handle wheel or touch events yourself.
  2. Match scroll-padding-inline to padding-inline so a snapped card lines up with the page content.
  3. Chrome 135 can draw the arrows and dots in CSS with ::scroll-button() and ::scroll-marker. Firefox and Safari have neither, so use the script.

Credits

Borrowed from: Steve Sewell, Build buttery smooth carousels with pure CSS like Nike. This version swaps his scrollIntoView() for scrollBy(), which can only scroll the carousel and never the page.

Docs: Adam Argyle, Carousels with CSS · MDN: scroll snap · MDN: scrollBy()