ComponentsData display

Carousel

A row or column of slides that scrolls one stop at a time. Use it for step cards, a strip of attachments, or a few cards side by side.

Usage

Carousel usageTSX
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@tulipfarm/sprout/carousel";

<div className="mx-auto w-full max-w-sm px-12">
  <Carousel aria-label="Growing steps">
    <CarouselContent>
      {["Plant", "Water", "Grow"].map((label, index) => (
        <CarouselItem key={label}>
          <div className="grid h-40 place-items-center rounded-xl border">
            {index + 1}. {label}
          </div>
        </CarouselItem>
      ))}
    </CarouselContent>
    <CarouselPrevious />
    <CarouselNext />
  </Carousel>
</div>

The carousel scrolls natively, so touch swipe, trackpads, and focus moving into a hidden slide all work. Slides snap into place unlessopts.dragFree is set. A mouse can drag the slides too.

Vertical

orientation="vertical" stacks slides in a column. Give the content a height, for example by wrapping it in a flex column with a fixed height. The arrows sit above and below and turn to point up and down.

Vertical carouselTSX
<div className="mx-auto w-full max-w-xs py-12">
  <Carousel orientation="vertical" aria-label="Growing steps">
    <div className="flex h-40 flex-col">
      <CarouselContent>{/* CarouselItem slides */}</CarouselContent>
    </div>
    <CarouselPrevious />
    <CarouselNext />
  </Carousel>
</div>

Options

  • opts.align: start (default),center, or end. Where the current slide lands.
  • opts.dragFree: scroll without snapping. Arrows and keys still step one slide.
  • opts.loop: Next on the last stop goes to the first, and Previous on the first goes to the last.
  • opts.startIndex: the slide shown first.

Sizing slides

Slides fill the viewport by default. Set CSS tokens on the carousel, or pass a className to CarouselItem:

  • --sprout-carousel-basis: slide size. auto lets each slide keep its own width, as an attachment row does.calc((100% - 16px) / 2) shows two per view.
  • --sprout-carousel-gap: space between slides. Default 16px.
  • --sprout-carousel-control-offset: how far outside the carousel the arrows sit. Default 48px. Leave that much room around it.
Attachment rowTSX
function AttachmentArrows() {
  const { scrollPrev, scrollNext, canScrollPrev, canScrollNext } = useCarousel();
  return (
    <>
      {canScrollPrev && (
        <button type="button" aria-label="Previous attachments" onClick={scrollPrev}>
          <ChevronLeft size={12} aria-hidden="true" />
        </button>
      )}
      {canScrollNext && (
        <button type="button" aria-label="Next attachments" onClick={scrollNext}>
          <ChevronRight size={12} aria-hidden="true" />
        </button>
      )}
    </>
  );
}

<Carousel
  aria-label="Attached files"
  opts={{ align: "start", dragFree: true }}
  className="[--sprout-carousel-basis:auto] [--sprout-carousel-gap:8px]"
>
  <CarouselContent className="py-1">
    {files.map((file) => (
      <CarouselItem key={file.id}>
        <FileAttachment variant="chip" file={file} />
      </CarouselItem>
    ))}
  </CarouselContent>
  <AttachmentArrows />
</Carousel>

API

useCarousel() works inside a carousel and returnsscrollPrev, scrollNext, scrollTo,canScrollPrev, canScrollNext,selectedIndex, snapCount,orientation, and api. Use it to draw your own arrows or a counter.

setApi hands the CarouselApi to a parent. It hasscrollPrev(), scrollNext(),scrollTo(index), canScrollPrev(),canScrollNext(), selectedScrollSnap(),scrollSnapList(), slideNodes(), andon/off for the select andreInit events. The carousel does not take Embla plugins.

Dots from setApiTSX
const [api, setApi] = useState<CarouselApi>();
const [selected, setSelected] = useState(0);

useEffect(() => {
  if (!api) return;
  const sync = (current: CarouselApi) => setSelected(current.selectedScrollSnap());
  api.on("select", sync);
  return () => {
    api.off("select", sync);
  };
}, [api]);

<Carousel setApi={setApi} opts={{ loop: true }} aria-label="Planting guides">
  …
</Carousel>

CarouselPrevious and CarouselNext areButtons, secondary and icon by default, and take any Button variant and size. They grow to 44px on touch screens.

Accessibility

The carousel is a region witharia-roledescription="carousel", and each slide is agroup with aria-roledescription="slide". Name the carousel with aria-label.

Left and Right step through slides while focus is inside, and Up and Down do the same when it is vertical. Keys typed into fields are left alone. The arrow buttons stay focusable when they turn off at an end, so focus does not jump away. When no slide holds a focusable element, the viewport itself takes focus, and Home and End jump to the first and last slide.

Scrolling animates only when reduced motion is off.