---
import { Card, CardContent } from "@/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
---
<Carousel class="w-full max-w-[12rem] sm:max-w-xs">
<CarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem>
<div class="p-1">
<Card>
<CardContent class="flex aspect-square items-center justify-center p-6">
<span class="text-4xl font-semibold">{index + 1}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>Installation
npx shadcn@latest add @fulldev/carousel
Usage
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
<Carousel aria-label="Featured slides">
<CarouselContent>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
The carousel runs the vanilla Embla Carousel
engine in a client script and needs no React. CarouselContent renders
the overflow viewport and an inner track; its classes and attributes apply to
the track.
Carousel renders a section with aria-roledescription="carousel". Give it
an aria-label or aria-labelledby so it becomes a named region, as the
carousel pattern
recommends; without a name it gets role="group".
Composition
Carousel
├── CarouselContent
│ ├── CarouselItem
│ └── CarouselItem
├── CarouselPrevious
└── CarouselNext
Sizes
Set the size of the items with a basis-* class on CarouselItem, such as
basis-1/3 or md:basis-1/2 lg:basis-1/3.
---
import { Card, CardContent } from "@/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
---
<Carousel
opts={{
align: "start",
}}
class="w-full max-w-[12rem] sm:max-w-xs md:max-w-sm"
>
<CarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem class="basis-1/2 lg:basis-1/3">
<div class="p-1">
<Card>
<CardContent class="flex aspect-square items-center justify-center p-6">
<span class="text-3xl font-semibold">{index + 1}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>Spacing
Use pl-[VALUE] on CarouselItem and a matching -ml-[VALUE] on
CarouselContent.
---
import { Card, CardContent } from "@/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
---
<Carousel class="w-full max-w-[12rem] sm:max-w-xs md:max-w-sm">
<CarouselContent class="-ml-1">
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem class="basis-1/2 pl-1 lg:basis-1/3">
<div class="p-1">
<Card>
<CardContent class="flex aspect-square items-center justify-center p-6">
<span class="text-2xl font-semibold">{index + 1}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>Orientation
Use the orientation prop to set the scroll axis. Give a vertical
CarouselContent an explicit height.
---
import { Card, CardContent } from "@/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
---
<Carousel
opts={{
align: "start",
}}
orientation="vertical"
class="w-full max-w-xs"
>
<CarouselContent class="-mt-1 h-[270px]">
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem class="basis-1/2 pt-1">
<div class="p-1">
<Card>
<CardContent class="flex items-center justify-center p-6">
<span class="text-3xl font-semibold">{index + 1}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>Options
Pass serializable Embla options
through opts, such as align, loop, startIndex, watchDrag,
slidesToScroll, direction and breakpoints. orientation sets axis.
Previous, next and keyboard navigation (arrow keys, Home, End) jump instead of
animate when the user prefers reduced motion.
<Carousel opts={{ align: "start", loop: true }}>
<CarouselContent>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
</CarouselContent>
</Carousel>
API
Listen for carousel:init on the root to receive the Embla API. For code that
runs after initialization, the root’s __fulldevCarouselApi property holds the
current API.
---
import { Card, CardContent } from "@/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
const count = 5
---
<div class="mx-auto max-w-[10rem] sm:max-w-xs" data-carousel-api-demo>
<Carousel class="w-full max-w-xs">
<CarouselContent>
{Array.from({ length: count }).map((_, index) => (
<CarouselItem>
<Card class="m-px">
<CardContent class="flex aspect-square items-center justify-center p-6">
<span class="text-4xl font-semibold">{index + 1}</span>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
<div
class="py-2 text-center text-sm text-muted-foreground"
data-carousel-api-status
>
Slide 1 of {count}
</div>
</div>
<script>
import type { CarouselApi } from "@/components/ui/carousel"
type CarouselRoot = HTMLElement & { __fulldevCarouselApi?: CarouselApi }
const initialize = () => {
for (const root of document.querySelectorAll("[data-carousel-api-demo]")) {
if (root.hasAttribute("data-bound")) continue
root.setAttribute("data-bound", "")
const carousel = root.querySelector<CarouselRoot>(
'[data-slot="carousel"]'
)
const status = root.querySelector("[data-carousel-api-status]")
if (!carousel || !status) continue
const update = (api: CarouselApi) => {
status.textContent = `Slide ${api.selectedScrollSnap() + 1} of ${api.scrollSnapList().length}`
}
if (carousel.__fulldevCarouselApi) update(carousel.__fulldevCarouselApi)
carousel.addEventListener("carousel:select", (event) =>
update((event as CustomEvent<{ api: CarouselApi }>).detail.api)
)
}
}
initialize()
document.addEventListener("astro:page-load", initialize)
</script>Events
carousel:select fires after each selection and 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:select", (event) => {
const { api } = (event as CustomEvent<{ api: CarouselApi }>).detail
// Do something on select.
})
Instances are destroyed before Astro page swaps and recreated on page load.
Plugins
Plugin instances are functions, so they cannot be passed as Astro props. This
example adds autoplay with a short script on the Embla API from
carousel:init: it advances every 2 seconds and stops on hover, drag or focus.
---
import { Card, CardContent } from "@/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
---
<Carousel
data-carousel-autoplay="2000"
class="w-full max-w-[10rem] sm:max-w-xs"
>
<CarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem>
<div class="p-1">
<Card>
<CardContent class="flex aspect-square items-center justify-center p-6">
<span class="text-4xl font-semibold">{index + 1}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
<script>
import type { CarouselApi } from "@/components/ui/carousel"
type CarouselRoot = HTMLElement & { __fulldevCarouselApi?: CarouselApi }
// Autoplay with delay 2000 and stopOnInteraction, like
// embla-carousel-autoplay: hovering, dragging or focusing a slide stops it.
const autoplay = (root: HTMLElement, api: CarouselApi) => {
const delay = Number(root.dataset.carouselAutoplay)
let timer = 0
let active = false
const play = () => {
clearTimeout(timer)
active = true
timer = window.setTimeout(() => {
if (api.canScrollNext()) api.scrollNext()
else api.scrollTo(0)
play()
}, delay)
}
const stop = () => {
clearTimeout(timer)
active = false
}
const reset = () => {
if (active) play()
}
root.addEventListener("mouseenter", stop)
root.addEventListener("mouseleave", reset)
api.on("pointerDown", stop)
api.on("slideFocusStart", stop)
api.on("destroy", stop)
if (api.scrollSnapList().length > 1) play()
}
const initialize = () => {
for (const root of document.querySelectorAll<CarouselRoot>(
"[data-carousel-autoplay]"
)) {
if (root.hasAttribute("data-bound")) continue
root.setAttribute("data-bound", "")
if (root.__fulldevCarouselApi) autoplay(root, root.__fulldevCarouselApi)
else
root.addEventListener(
"carousel:init",
(event) =>
autoplay(
root,
(event as CustomEvent<{ api: CarouselApi }>).detail.api
),
{ once: true }
)
}
}
initialize()
document.addEventListener("astro:page-load", initialize)
</script>RTL
Set dir on Carousel and the matching direction option in opts so the
carousel scrolls in the reading direction.
---
import { Card, CardContent } from "@/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
const translations = {
en: {
dir: "ltr",
values: {},
},
ar: {
dir: "rtl",
values: {},
},
he: {
dir: "rtl",
values: {},
},
} as const
function toArabicNumerals(num: number): string {
const arabicNumerals = ["٠", "١", "٢", "٣", "٤", "٥", "٦", "٧", "٨", "٩"]
return num
.toString()
.split("")
.map((digit) => arabicNumerals[parseInt(digit, 10)])
.join("")
}
const language = "ar"
const { dir } = translations[language]
const formatNumber = (num: number): string => {
if (language === "ar") {
return toArabicNumerals(num)
}
return num.toString()
}
---
<Carousel
dir={dir}
class="w-full max-w-[12rem] sm:max-w-xs"
opts={{
direction: dir,
}}
>
<CarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem>
<div class="p-1">
<Card>
<CardContent class="flex aspect-square items-center justify-center p-6">
<span class="text-4xl font-semibold">
{formatNumber(index + 1)}
</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>API Reference
Source: src/components/ui/carousel
| Prop | Type | Default |
|---|---|---|
opts | CarouselOptions | undefined |
orientation | "horizontal" | "vertical" | "horizontal" |
CarouselPrevious and CarouselNext accept the Button variant and size
props (default "outline" and "icon-sm") and are disabled automatically when
the carousel cannot scroll that way, unless you pass disabled. The root
exposes data-orientation, data-can-scroll-prev, data-can-scroll-next and
data-selected-scroll-snap. CarouselApi, CarouselOptions and
CarouselPlugin re-export Embla’s types.
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 |