Build swipeable galleries on Embla. Compose the slides, then place previous and next controls beside the track or in a row below it.
import { Card, CardContent } from "@/components/ui/card"import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious,} from "@/components/ui/carousel"export function Example() { return ( <Carousel className="w-full max-w-xs"> <CarouselContent> {Array.from({ length: 5 }).map((_, index) => ( <CarouselItem key={index}> <Card> <CardContent className="flex aspect-square items-center justify-center p-6"> <span className="text-4xl font-normal tracking-wide"> {index + 1} </span> </CardContent> </Card> </CarouselItem> ))} </CarouselContent> <CarouselPrevious className="absolute -left-12 top-1/2 -translate-y-1/2" /> <CarouselNext className="absolute -right-12 top-1/2 -translate-y-1/2" /> </Carousel> )}Installation
npx @arctis-sh/@arctis-sh/ui@latest add carouselInstalls with Button and requires the embla-carousel-react peer dependency. The plugins example also uses embla-carousel-autoplay.
Usage
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious,} from "@/components/ui/carousel"<Carousel> <CarouselContent> <CarouselItem>...</CarouselItem> <CarouselItem>...</CarouselItem> <CarouselItem>...</CarouselItem> </CarouselContent> <CarouselPrevious className="absolute -left-12 top-1/2 -translate-y-1/2" /> <CarouselNext className="absolute -right-12 top-1/2 -translate-y-1/2" /></Carousel>Composition
Carousel ├── CarouselContent │ ├── CarouselItem │ └── CarouselItem ├── CarouselPrevious ├── CarouselNext ├── CarouselControls └── CarouselStatus
Controls
CarouselControls places previous and next controls below the track.
import { Card, CardContent } from "@/components/ui/card"import { Carousel, CarouselContent, CarouselControls, CarouselItem,} from "@/components/ui/carousel"export function Example() { return ( <Carousel className="w-full max-w-xs"> <CarouselContent> {Array.from({ length: 5 }).map((_, index) => ( <CarouselItem key={index}> <Card> <CardContent className="flex aspect-square items-center justify-center p-6"> <span className="text-4xl font-normal tracking-wide"> {index + 1} </span> </CardContent> </Card> </CarouselItem> ))} </CarouselContent> <CarouselControls /> </Carousel> )}Pass showStatus to display Slide N of M between them with CarouselStatus.
import { Card, CardContent } from "@/components/ui/card"import { Carousel, CarouselContent, CarouselControls, CarouselItem,} from "@/components/ui/carousel"export function Example() { return ( <Carousel className="w-full max-w-xs"> <CarouselContent> {Array.from({ length: 5 }).map((_, index) => ( <CarouselItem key={index}> <Card> <CardContent className="flex aspect-square items-center justify-center p-6"> <span className="text-4xl font-normal tracking-wide"> {index + 1} </span> </CardContent> </Card> </CarouselItem> ))} </CarouselContent> <CarouselControls showStatus /> </Carousel> )}Loop
Set opts.loop to continue smoothly from the last slide to the first.
import { Card, CardContent } from "@/components/ui/card"import { Carousel, CarouselContent, CarouselControls, CarouselItem,} from "@/components/ui/carousel"export function Example() { return ( <Carousel opts={{ loop: true }} className="w-full max-w-xs"> <CarouselContent> {Array.from({ length: 5 }).map((_, index) => ( <CarouselItem key={index}> <Card> <CardContent className="flex aspect-square items-center justify-center p-6"> <span className="text-4xl font-normal tracking-wide"> {index + 1} </span> </CardContent> </Card> </CarouselItem> ))} </CarouselContent> <CarouselControls showStatus /> </Carousel> )}Sizes
Set a basis on CarouselItem to change slide width. Use opts.align: "start" when the frame shows more than one slide.
import { Card, CardContent } from "@/components/ui/card"import { Carousel, CarouselContent, CarouselControls, CarouselItem,} from "@/components/ui/carousel"export function Example() { return ( <Carousel opts={{ align: "start" }} className="w-full max-w-sm"> <CarouselContent> {Array.from({ length: 5 }).map((_, index) => ( <CarouselItem key={index} className="basis-1/3"> <Card> <CardContent className="flex aspect-square items-center justify-center p-4"> <span className="text-2xl font-normal tracking-wide"> {index + 1} </span> </CardContent> </Card> </CarouselItem> ))} </CarouselContent> <CarouselControls /> </Carousel> )}<CarouselItem className="basis-1/3">...</CarouselItem><CarouselItem className="md:basis-1/2 lg:basis-1/3">...</CarouselItem>Spacing
Create space between slides with matching -ml-[VALUE] on CarouselContent and pl-[VALUE] on each CarouselItem.
import { Card, CardContent } from "@/components/ui/card"import { Carousel, CarouselContent, CarouselControls, CarouselItem,} from "@/components/ui/carousel"export function Example() { return ( <Carousel opts={{ align: "start" }} className="w-full max-w-sm"> <CarouselContent className="-ml-8"> {Array.from({ length: 5 }).map((_, index) => ( <CarouselItem key={index} className="basis-1/2 pl-8"> <Card> <CardContent className="flex aspect-square items-center justify-center p-4"> <span className="text-2xl font-normal tracking-wide"> {index + 1} </span> </CardContent> </Card> </CarouselItem> ))} </CarouselContent> <CarouselControls /> </Carousel> )}<CarouselContent className="-ml-8"> <CarouselItem className="basis-1/2 pl-8">...</CarouselItem></CarouselContent>Orientation
Set orientation="vertical" for a vertical track. Give CarouselContent a fixed height and use basis-full to fill the frame with one slide.
import { Card, CardContent } from "@/components/ui/card"import { Carousel, CarouselContent, CarouselControls, CarouselItem,} from "@/components/ui/carousel"export function Example() { return ( <Carousel opts={{ align: "start" }} orientation="vertical" className="w-full" > <CarouselContent className="h-[220px]"> {Array.from({ length: 5 }).map((_, index) => ( <CarouselItem key={index} className="basis-full pt-0"> <Card className="h-full"> <CardContent className="flex h-full items-center justify-center p-6"> <span className="text-3xl font-normal tracking-wide"> {index + 1} </span> </CardContent> </Card> </CarouselItem> ))} </CarouselContent> <CarouselControls /> </Carousel> )}Options
Pass Embla options directly through opts.
<Carousel opts={{ align: "start", loop: true, }}> <CarouselContent> <CarouselItem>...</CarouselItem> </CarouselContent></Carousel>API
Use setApi to keep the Embla API in state for slide indexes, counts, and custom controls.
Slide 0 of 0
import { useEffect, useState } from "react"import { Card, CardContent } from "@/components/ui/card"import { Carousel, CarouselContent, CarouselControls, CarouselItem, type CarouselApi,} from "@/components/ui/carousel"export function Example() { const [api, setApi] = useState<CarouselApi>() const [current, setCurrent] = useState(0) const [count, setCount] = useState(0) useEffect(() => { if (!api) return setCount(api.scrollSnapList().length) setCurrent(api.selectedScrollSnap() + 1) api.on("select", () => { setCurrent(api.selectedScrollSnap() + 1) }) }, [api]) return ( <div className="w-full max-w-xs"> <Carousel setApi={setApi}> <CarouselContent> {Array.from({ length: 5 }).map((_, index) => ( <CarouselItem key={index}> <Card> <CardContent className="flex aspect-square items-center justify-center p-6"> <span className="text-4xl font-normal tracking-wide"> {index + 1} </span> </CardContent> </Card> </CarouselItem> ))} </CarouselContent> <CarouselControls /> </Carousel> <p className="mt-2 text-center text-sm text-muted-foreground"> Slide {current} of {count} </p> </div> )}import { useEffect, useState } from "react"import { type CarouselApi } from "@/components/ui/carousel"const [api, setApi] = useState<CarouselApi>()const [current, setCurrent] = useState(0)const [count, setCount] = useState(0)useEffect(() => { if (!api) return setCount(api.scrollSnapList().length) setCurrent(api.selectedScrollSnap() + 1) api.on("select", () => { setCurrent(api.selectedScrollSnap() + 1) })}, [api])return <Carousel setApi={setApi}>...</Carousel>Events
Subscribe to events on the API returned through setApi. Embla documents the full event list.
useEffect(() => { if (!api) return api.on("select", () => { // Do something on select. })}, [api])Plugins
Pass Embla plugins through the plugins prop.
import Autoplay from "embla-carousel-autoplay"import { Card, CardContent } from "@/components/ui/card"import { Carousel, CarouselContent, CarouselControls, CarouselItem,} from "@/components/ui/carousel"export function Example() { return ( <Carousel plugins={[ Autoplay({ delay: 2000, }), ]} className="w-full max-w-xs" > <CarouselContent> {Array.from({ length: 5 }).map((_, index) => ( <CarouselItem key={index}> <Card> <CardContent className="flex aspect-square items-center justify-center p-6"> <span className="text-4xl font-normal tracking-wide"> {index + 1} </span> </CardContent> </Card> </CarouselItem> ))} </CarouselContent> <CarouselControls /> </Carousel> )}import Autoplay from "embla-carousel-autoplay"<Carousel plugins={[ Autoplay({ delay: 2000, }), ]}> ...</Carousel>API Reference
Carousel
The root container that provides carousel context to its children.
| Prop | Type | Default |
|---|---|---|
| orientation | "horizontal" | "vertical" | "horizontal" |
| opts | CarouselOptions | — |
| plugins | CarouselPlugin | — |
| setApi | (api: CarouselApi) => void | — |
| className | string | — |
opts and plugins are Embla types from embla-carousel-react.
<Carousel> <CarouselContent> <CarouselItem>Slide 1</CarouselItem> </CarouselContent> <CarouselPrevious /> <CarouselNext /></Carousel>CarouselContent
Contains the viewport and slide track. Put horizontal spacing classes such as -ml-* here, or height classes for a vertical carousel.
| Prop | Type | Default |
|---|---|---|
| className | string | — |
<CarouselContent> <CarouselItem>Slide 1</CarouselItem></CarouselContent>CarouselItem
A single slide, full width by default with basis-full. Use another basis-* value for multi-slide layouts.
| Prop | Type | Default |
|---|---|---|
| className | string | — |
<CarouselItem>Slide 1</CarouselItem>CarouselPrevious
The previous control, built on Button. It disables when there is nowhere to scroll back.
| Prop | Type | Default |
|---|---|---|
| variant | ButtonVariant | "outline" |
| size | ButtonSize | "icon-sm" |
| className | string | — |
<CarouselPrevious />CarouselNext
The next control, built on Button. It disables when there is nowhere to scroll forward.
| Prop | Type | Default |
|---|---|---|
| variant | ButtonVariant | "outline" |
| size | ButtonSize | "icon-sm" |
| className | string | — |
<CarouselNext />CarouselControls
A bottom row for previous and next controls, with an optional status in the center.
| Prop | Type | Default |
|---|---|---|
| showStatus | boolean | false |
| className | string | — |
<CarouselControls> <CarouselPrevious /> <CarouselNext /></CarouselControls>CarouselStatus
Renders Slide N of M from the Embla API. It is usually enabled with CarouselControls showStatus.
| Prop | Type | Default |
|---|---|---|
| className | string | — |
<CarouselStatus />CarouselApi
Type alias for the Embla API returned by setApi. Use it for methods such as selectedScrollSnap, scrollSnapList, scrollNext, and scrollPrev, plus events such as select.
const [api, setApi] = useState<CarouselApi>()<Carousel setApi={setApi}> ...</Carousel>