Carousel
Slides you swipe, drag or step through, with momentum, snapping, loop and autoplay.
Carousel
Drag or swipe, use the buttons, or focus inside and press the arrow keys.
Code
<Carousel label="Numbers" class="w-full max-w-xs">
<CarouselContent>
<CarouselItem e:for={n of [1, 2, 3, 4, 5]}>
<div class="card">{n}</div>
</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>Usage
import { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext } from "@elements/ui/carousel";
<Carousel label="Featured" opts={{ loop: true }}>
<CarouselContent>
<CarouselItem e:for={item of items}>...</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
<!-- index is two-way; autoplay adds the rotation control -->
<Carousel index={state.slide} autoplay={4000} dots>...</Carousel>Examples
Sizes
Set --carousel-item-size for slides narrower than the track; align start keeps the first flush.
Code
<!-- --carousel-item-size sets every slide's flex-basis -->
<Carousel opts={{ align: "start" }} class="thirds">
<CarouselContent>
<CarouselItem e:for={n of items}>...</CarouselItem>
</CarouselContent>
<CarouselPrevious size="sm" />
<CarouselNext size="sm" />
</Carousel>
.thirds { --carousel-item-size: 33.333%; }
@media (max-width: 40rem) { .thirds { --carousel-item-size: 50%; } }Options and breakpoints
Changing opts re-initializes the carousel on the slide it was on. Slides are one, two or three across depending on the window width.
Code
<!-- opts can change at runtime: the carousel re-initializes and keeps its slide.
A class per breakpoint sizes the slides: half on medium screens, a third on large. -->
<Carousel opts={state.loop ? { align: "start", loop: true } : { align: "start" }} class="responsive">
...
</Carousel>
<Button onclick={() => state.loop = !state.loop}>Loop</Button>
.responsive { --carousel-item-size: 100%; }
@media (min-width: 30rem) { .responsive { --carousel-item-size: 50%; } }
@media (min-width: 48rem) { .responsive { --carousel-item-size: 33.333%; } }
/* Spacing per breakpoint too */
.responsive { --carousel-gap: var(--space-2); }
@media (min-width: 48rem) { .responsive { --carousel-gap: var(--space-4); } }Spacing
--carousel-gap sets the space between slides.
Code
<Carousel class="tight">...</Carousel>
.tight { --carousel-gap: var(--space-1); }Orientation
orientation vertical scrolls up and down; ArrowUp and ArrowDown step.
Code
<Carousel orientation="vertical" opts={{ align: "start" }}>
<CarouselContent class="h-52">
<CarouselItem e:for={n of items} class="half">...</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>API
index is two-way; onApi hands over the carousel's API (here, for the slide count).
Slide 1 of 0; onSelect: 0
Code
<Carousel index={state.index} onApi={(api) => state.count = api.scrollSnapList().length}>
...
</Carousel>
<p>Slide {state.index + 1} of {state.count}</p>
<Button onclick={() => state.index = 0}>First</Button>Pickers and loop
dots adds a button per slide (APG grouped carousel); loop wraps around.
Code
<Carousel label="Gallery" dots opts={{ loop: true }}>...</Carousel>Autoplay
autoplay rotates the slides and adds the rotation control. Hovering pauses; keyboard focus inside stops it until the control starts it.
Code
<!-- autoplay is the delay in ms; it adds the rotation control (APG) -->
<Carousel label="Highlights" autoplay={2000} opts={{ loop: true }} dots>...</Carousel>Plugins
plugins takes carousel plugins: the Autoplay plugin on its own, with no rotation control. Hovering stops it, leaving starts it again, and a press stops it for good. Prefer the autoplay attribute when people need to stop it.
Code
import { Autoplay } from "@elements/ui/carousel";
<Page (
private plugins = [Autoplay({ delay: 2000, stopOnInteraction: true })],
private ui = { pressed: false },
)>
<div onpointerenter={() => pluginEnter(plugins)}
onpointerleave={() => pluginLeave(plugins, ui)}
onpointerdown={() => pluginPress(plugins, ui)}>
<Carousel label="Plugins" plugins={plugins} opts={{ loop: true }}>...</Carousel>
</div>
</Page>Right to left
Under rtl the first slide is on the right, the buttons mirror, and ArrowLeft moves forward. Switching dir re-initializes the carousel the other way.
Code
<Carousel dir={state.dir}>...</Carousel>
<Button onclick={() => state.dir = state.dir === "rtl" ? "ltr" : "rtl"}>Direction</Button>Tabbed pickers
pickers tabs makes the pickers a tablist and the slides tabpanels (APG tabbed carousel). Arrow keys move between tabs; each tab takes its slide's label.
Code
<!-- APG tabbed carousel: the pickers are a tablist, the slides tabpanels -->
<Carousel label="Photos" dots pickers="tabs" autoplay={5000} opts={{ loop: true }}>
<CarouselContent>
<CarouselItem e:for={p of photos} label={p}>...</CarouselItem>
</CarouselContent>
</Carousel>Events
onApi hands over the carousel's API; subscribe with api.on. This one is a group named by its visible heading, and lists each event the first time it fires.
Events
Code
<h4 id="events-title">Events</h4>
<Carousel role="group" attrs={{ "aria-labelledby": "events-title" }} onApi={(api) => {
api.on("select", () => console.log(api.selectedScrollSnap()));
api.on("settle", () => ...);
}}>...</Carousel>Global options
carouselGlobalOptions sets options for every carousel created after it, under each one's own opts. A selector narrows them to part of the page: this carousel sets no opts and loops.
Code
import { carouselGlobalOptions } from "@elements/ui/carousel";
// Every carousel created from now on loops unless its opts say otherwise.
carouselGlobalOptions({ loop: true });
// Or only the carousels inside elements matching a selector.
carouselGlobalOptions({ loop: true }, "[data-carousel-global]");Fade
The Fade plugin cross-fades the slides in place instead of sliding them. Drag, the buttons and the arrow keys all fade.
Code
import { Fade } from "@elements/ui/carousel";
<Carousel label="Fade" plugins={[Fade()]}>...</Carousel>Auto height
The AutoHeight plugin sizes the carousel to the selected slide, so short slides leave no empty space. The content below moves when the height changes.
Code
import { AutoHeight } from "@elements/ui/carousel";
<!-- The track takes the selected slide's height; the page below moves. -->
<Carousel label="Auto height" plugins={[AutoHeight()]}>...</Carousel>Class names
The ClassNames plugin puts the engine's state classes on the carousel for a stylesheet: is-snapped and is-in-view on slides, is-draggable and is-dragging on the viewport. Here slides that are not snapped are dimmed.
Code
import { ClassNames } from "@elements/ui/carousel";
<Carousel label="Class names" plugins={[ClassNames()]} opts={{ align: "start" }}>...</Carousel>
.carousel-item:not(.is-snapped) .card { opacity: 0.5; }
.carousel-viewport.is-draggable { cursor: grab; }
.carousel-viewport.is-dragging { cursor: grabbing; }Auto scroll
The AutoScroll plugin moves the track continuously, for a ticker. Hovering or focus inside stops it, and the button pauses and plays it (moving content needs a pause control).
Code
import { AutoScroll } from "@elements/ui/carousel";
<Page (private scroller = AutoScroll({ speed: 1, startDelay: 0, stopOnMouseEnter: true }))>
<Carousel label="Ticker" plugins={[scroller]} opts={{ loop: true, dragFree: true }}>...</Carousel>
<!-- Moving content needs a pause control (WCAG 2.2.2). -->
<Button onclick={() => scroller.isPlaying() ? scroller.stop() : scroller.play(0)}>Pause</Button>
</Page>Wheel gestures
The WheelGestures plugin turns a sideways trackpad swipe into a drag that lands on a slide. A vertical scroll over it still scrolls the page.
Code
import { WheelGestures } from "@elements/ui/carousel";
<!-- A sideways trackpad swipe steps the slides; a vertical scroll is the page's. -->
<Carousel label="Wheel" plugins={[WheelGestures()]}>...</Carousel>