elements
Demos UI Feed Discuss Install
Style System
  • Style
  • Themes
  • Behaviors
Forms
  • Button
  • ButtonGroup
  • Checkbox
  • ColorPicker
  • Combobox
  • Field
  • FileUpload
  • Input
  • InputGroup
  • InputOTP
  • Label
  • NativeSelect
  • NumberInput
  • PasswordInput
  • RadioGroup
  • Select
  • Slider
  • Switch
  • TagsInput
  • Textarea
  • Toggle
  • ToggleGroup
Overlays
  • AlertDialog
  • Command
  • ContextMenu
  • Dialog
  • Drawer
  • DropdownMenu
  • HoverCard
  • Menubar
  • NavigationMenu
  • Popover
  • Sheet
  • Toast
  • Tooltip
Display
  • Accordion
  • Alert
  • AspectRatio
  • Avatar
  • Badge
  • Breadcrumb
  • Card
  • Collapsible
  • Empty
  • Item
  • Kbd
  • Pagination
  • Progress
  • Separator
  • Skeleton
  • Spinner
  • Table
  • Tabs
  • Tree
Layout
  • Carousel
  • Resizable
  • ScrollArea
  • Sidebar
Dates
  • Calendar
  • DatePicker
  • DateRangePicker
  • TimePicker
Data
  • AreaChart
  • BarChart
  • Chart
  • DataTable
  • Kanban
  • LineChart
  • PieChart
  • RadarChart
  • RadialChart
  • VirtualList
Chat
  • Attachment
  • Message
  • MessageList
  • PromptInput
Elements UI / Layout / Carousel

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.

1
2
3
4
5
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.

1
2
3
4
5
6
7
8
9
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.

1
2
3
4
5
6
7
8
9
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.

1
2
3
4
5
Code
<Carousel class="tight">...</Carousel>

.tight { --carousel-gap: var(--space-1); }

Orientation

orientation vertical scrolls up and down; ArrowUp and ArrowDown step.

1
2
3
4
5
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).

1
2
3
4
5

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.

1
2
3
4
5
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.

1
2
3
4
5
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.

1
2
3
4
5
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.

Harbor at dawn
Market street
Hill road
Night ferry
Old lighthouse
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.

1
2
3
4
5
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.

1
2
3
4
5
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.

1
2
3
4
5
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.

1
2
3
4
5
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).

1
2
3
4
5
6
7
8
9
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.

1
2
3
4
5
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>
Reference Carousel in the manual Every part, attribute, slot, key and data attribute.
Prev TreeNext Resizable
Product
Manual Pricing Compare
Company
History Contact
Legal
Privacy Policy Acceptable Use Terms License Agreement
© Elements Dev Corporation 2026