---
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
---
<Carousel class="w-[calc(100%-2rem)] max-w-xl">
<CarouselContent>
<CarouselItem>
<div
class="bg-muted flex aspect-video items-center justify-center rounded-md"
>
<span class="text-3xl font-semibold">1</span>
</div>
</CarouselItem>
<CarouselItem>
<div
class="bg-muted flex aspect-video items-center justify-center rounded-md"
>
<span class="text-3xl font-semibold">2</span>
</div>
</CarouselItem>
<CarouselItem>
<div
class="bg-muted flex aspect-video items-center justify-center rounded-md"
>
<span class="text-3xl font-semibold">3</span>
</div>
</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>About
The carousel uses the vanilla Embla Carousel engine, following shadcn/ui composition and CSS slide sizing. It needs no React. Other interactive Fulldev UI components continue to use Data Slot.
CarouselContent provides an overflow viewport and an inner track. Classes and
HTML attributes passed to CarouselContent apply to the track. Use responsive
basis-* classes on items and matching negative track margins / slide padding
for spacing. No multiple option is needed.
Installation
Install the component from the @fulldev registry with the shadcn CLI:
npx shadcn@latest add @fulldev/carousel
Manual install:
npm install embla-carousel
Then copy the component files into src/components/ui/carousel and update the
imports to match your project structure.
Usage
---
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
---
<Carousel>
<CarouselContent>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
Composition
Carousel
├── CarouselContent
│ ├── CarouselItem
│ └── CarouselItem
├── CarouselPrevious
└── CarouselNext
Options
Pass serializable Embla options through opts:
align, loop, startIndex, watchDrag, slidesToScroll and responsive
breakpoints are supported. orientation="vertical" sets the scrolling axis;
otherwise opts.axis is respected. Dragging and focus scrolling use Embla’s
defaults. Previous/next and keyboard navigation respect reduced motion.
---
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
---
<Carousel
opts={{ startIndex: 1, loop: true }}
class="w-[calc(100%-2rem)] max-w-xl"
>
<CarouselContent>
<CarouselItem
class="bg-muted flex min-h-40 items-center justify-center rounded-md"
>First slide</CarouselItem
>
<CarouselItem
class="bg-muted flex min-h-40 items-center justify-center rounded-md"
>Second slide</CarouselItem
>
<CarouselItem
class="bg-muted flex min-h-40 items-center justify-center rounded-md"
>Third slide</CarouselItem
>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>Vertical
Give a vertical CarouselContent an explicit height so each item can fill its
viewport.
---
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
---
<Carousel orientation="vertical" class="my-8 w-full max-w-xl">
<CarouselContent class="h-64">
<CarouselItem
class="bg-muted flex min-h-40 items-center justify-center rounded-md"
>First slide</CarouselItem
>
<CarouselItem
class="bg-muted flex min-h-40 items-center justify-center rounded-md"
>Second slide</CarouselItem
>
<CarouselItem
class="bg-muted flex min-h-40 items-center justify-center rounded-md"
>Third slide</CarouselItem
>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>Events and API
Listen for carousel:init to receive the Embla API. carousel:select fires after
selection or reinitialization. Both events expose detail.api, canScrollPrev,
canScrollNext, selectedScrollSnap and scrollSnapList.
import type { CarouselApi } from "@/components/ui/carousel"
const carousel = document.querySelector('[data-slot="carousel"]')
carousel?.addEventListener("carousel:init", (event) => {
const { api } = (event as CustomEvent<{ api: CarouselApi }>).detail
api.scrollTo(2)
})
Register the listener before initialization. For code running later, the root’s
__fulldevCarouselApi property holds the current API. CarouselOptions and
CarouselPlugin re-export Embla’s corresponding types. Plugin instances are
configured through Embla’s API, not serialized Astro props.
Instances are destroyed before Astro page swaps and recreated on page load.
Visible links retain normal focus behavior; focusing an offscreen slide lets
Embla bring it into view. No custom inert or aria-hidden overrides are added.
Multiple visible slides
Use CSS widths just as in shadcn/ui. This example shows three clickable slides at once, with extra slides reachable through dragging, buttons or keyboard.
---
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
---
<Carousel
data-demo="multiple"
opts={{ align: "start" }}
class="w-[calc(100%-2rem)] max-w-xl"
>
<CarouselContent>
{
[1, 2, 3, 4, 5].map((number) => (
<CarouselItem class="basis-1/3">
<a
href={`#slide-${number}`}
class="bg-muted flex aspect-square items-center justify-center rounded-md"
>
Slide {number}
</a>
</CarouselItem>
))
}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>Migration from 0.13
Reinstall the registry component so markup and runtime stay together.
| 0.13 | Embla carousel |
|---|---|
defaultIndex={1} | opts={{ startIndex: 1 }} |
loop | opts={{ loop: true }} |
drag | Enabled by default; disable with opts={{ watchDrag: false }} |
CarouselController | CarouselApi |
carousel:change | carousel:select |
carousel:set | Call methods on detail.api from carousel:init |
| Content classes style the viewport | Content classes style the inner track |
The former one-full-width-slide restriction no longer applies. Use matching negative margins on content and padding on items for spacing, like shadcn/ui.