Skip to content
WANCRAFT

B.157Cards & Containers

Snap Carousel

An accessible carousel on native CSS scroll-snap with peeking slides, dot or thumbnail pickers and pausable autoplay.

NewReactCSSTailwindintermediate

Overview

Snap Carousel lets the browser do the hard parts: the track is a native scroll-snap container, so touch swipes, trackpads and momentum feel exactly like the platform. An IntersectionObserver reports which slides are fully in view, which drives the dot or thumbnail picker, the position counter and the prev / next buttons, which disable at either end. Slides per view respond to the carousel's own width through container queries, a configurable slice of the next slide peeks in, and slides snap to the start or the centre. Optional autoplay runs once to the last slide rather than looping, shows its progress in a ring around a pause button, and pauses on hover, on focus, in a hidden tab and offscreen. It follows the APG carousel pattern: a region with aria-roledescription="carousel" and slides as labelled groups.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/snap-carousel.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/snap-carousel.json

Method 02 · Copy and paste

  1. 1. No extra dependencies — React and Tailwind CSS only.
  2. 2. Copy the source from the Code tab into components/ui/snap-carousel.tsx.
  3. 3. Import it and adjust props.
Dependencies
None
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { SnapCarousel, type SnapCarouselSlide } from "@/components/ui/snap-carousel";

export function SnapCarouselExample() {
  const slides: SnapCarouselSlide[] = ["Arc Lamp", "Halden Chair", "Tide Mug", "Ono Speaker"].map((name) => ({
    id: name,
    label: name,
    content: <div className="flex aspect-[4/3] items-end rounded-2xl border p-4">{name}</div>,
  }));

  return (
    <SnapCarousel slides={slides} label="New arrivals" />
  );
}

03Props

PropTypeDefaultDescription
slidesSnapCarouselSlide[]—Required. { id, content, label?, thumbnail? }. label is announced with the position; thumbnail feeds the thumbnail picker.
labelstring"Carousel"Accessible name of the carousel region.
slidesPerViewnumber3Slides per view in a wide container (≥ 720px); 2 from 440px, 1 below.
peeknumber40Visible slice of the next slide in px (on both sides when centred).
gapnumber16Gap between slides in px.
align"start" | "center""start"Snap slides to the start edge or the centre.
indicators"dots" | "thumbnails" | "none""dots"Slide picker under the track.
autoplaybooleanfalseAdvance automatically until the last slide; adds a pause / play button with a progress ring.
intervalnumber5000Time per slide in ms while autoplaying.
accentstring"#FF7448"Active indicator, progress ring and focus ring colour.
onIndexChange(index: number) => void—Called when the active slide changes.
...propsHTMLAttributes<HTMLElement>—Forwarded to the root <section>.

04Features

  • 01Native CSS scroll-snap track: real touch, trackpad and momentum scrolling
  • 02IntersectionObserver keeps the picker, counter and active slide in sync with what is fully visible
  • 03Prev / next buttons disable at the ends; rapid clicks step from the slide being scrolled to
  • 04Container-query slides per view (1 → 2 → N) with a peek of the next slide
  • 05Dots or thumbnails, with in-view slides marked when several are visible
  • 06Run-once autoplay with a progress ring that pauses on hover, focus, hidden tab and offscreen
motion · scroll-linkedmotion · tweeninput · scrollinput · draginput · clickinput · keyboardinput · hoverinput · focus

05Accessibility

  • APG carousel semantics: a <section> with aria-roledescription="carousel" and a label; each slide is a role="group" with aria-roledescription="slide" named "2 of 6: Arc Lamp".
  • The track is focusable; Left / Right arrows move one slide, Home / End jump to the first and last.
  • Picker buttons are named "Slide 3: …" and mark the active one with aria-current; prev / next are real disabled buttons at the ends.
  • Autoplay has a visible pause / play button (WCAG 2.2.2), pauses while the pointer or focus is inside, and stays silent (aria-live off) while rotating; user navigation is announced politely.
  • With prefers-reduced-motion, autoplay waits for the play button and all scrolling is instant.

06Limitations

  • Autoplay does not loop: it stops on the last slide, and pressing play again restarts from the first.
  • Slides per view breakpoints are fixed at 440px and 720px of container width.