Tomatoes
Sow indoors 6 weeks before the last frost.
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.
One slide at a time, sideways or up and down. Use the arrows, the arrow keys, a swipe, or drag with a mouse. The arrows turn off at either end.
Slide 1 of 1
Slide 1 of 1
Staged files in one free-scrolling row: chips keep their own width, sit 8px apart, and align to the start. Edge arrows from useCarousel appear on hover or focus and hide at either end.
Set --sprout-carousel-basis to show more than one card. opts.loop wraps at the ends, and setApi drives the dots and the counter.
Sow indoors 6 weeks before the last frost.
Pinch the tips weekly so it grows bushy.
Sow a short row every two weeks.
Thin seedlings to 5 cm once they are 3 cm tall.
Give them a trellis before they start to climb.
Position 1 of 1
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.
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.
<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>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.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.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>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.
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.
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.