Shadcn Carousel
Free shadcn/ui carousel for React — built on Embla. Image and product carousels, multiple slides per view, vertical orientation, dot indicators, autoplay, and a synced thumbnail strip.
Installation
{
"registries": {
"@designrevision": "https://registry.designrevision.com/r/{name}.json"
}
}
Add to your existing components.json. Requires shadcn/ui v2.3+.
Packages
Props
orientation
= "horizontal"
"horizontal" | "vertical"
Scroll axis of the carousel.
opts
= —
EmblaOptionsType
Embla options — e.g. { loop: true, align: "start" }.
plugins
= —
EmblaPluginType[]
Embla plugins, e.g. Autoplay() from embla-carousel-autoplay.
setApi
= —
(api: CarouselApi) => void
Receive the Embla API to read/drive the carousel (dots, current slide).
A carousel built on Embla — compose <Carousel> with <CarouselContent> wrapping <CarouselItem>s, plus <CarouselPrevious> / <CarouselNext>. Each item is basis-full by default; set basis-1/2 or basis-1/3 on CarouselItem to show multiple per view. Pass opts (e.g. { loop: true }), orientation for a vertical carousel, plugins (Autoplay) for autoplay, and setApi to read the Embla API for dot indicators or the current slide. Arrow keys scroll it; slides carry the right ARIA roles.
"use client"
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) return
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) return
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel()
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel()
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
)
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
)
}
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}
Examples
Basic
The canonical carousel — slides with previous/next arrows, one per view.
Packages
Props
No props documented yet.
import { Card, CardContent } from "@/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
export default function CarouselDemo01() {
return (
<Carousel className="w-full max-w-xs">
<CarouselContent>
{Array.from({ length: 5 }).map((_, i) => (
<CarouselItem key={i}>
<Card>
<CardContent className="flex aspect-square items-center justify-center p-6">
<span className="text-4xl font-semibold">{i + 1}</span>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}
Multiple per view
Show several slides at once — `basis-1/3` on CarouselItem with `align: "start"`.
Packages
Props
No props documented yet.
import { Card, CardContent } from "@/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
export default function CarouselSizes01() {
return (
<Carousel opts={{ align: "start" }} className="w-full max-w-sm">
<CarouselContent>
{Array.from({ length: 6 }).map((_, i) => (
<CarouselItem key={i} className="basis-1/3">
<Card>
<CardContent className="flex aspect-square items-center justify-center p-6">
<span className="text-2xl font-semibold">{i + 1}</span>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}
Vertical
A vertical carousel — `orientation="vertical"` with a fixed-height content area.
Packages
Props
No props documented yet.
import { Card, CardContent } from "@/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
export default function CarouselVertical01() {
return (
<Carousel
orientation="vertical"
opts={{ align: "start" }}
className="w-full max-w-xs"
>
<CarouselContent className="h-[260px]">
{Array.from({ length: 5 }).map((_, i) => (
<CarouselItem key={i} className="basis-1/2">
<Card>
<CardContent className="flex items-center justify-center p-6">
<span className="text-3xl font-semibold">{i + 1}</span>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}
Dot indicators
Dot navigation and the current slide, read from the Embla API via `setApi`.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { Card, CardContent } from "@/components/ui/card"
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
export default function CarouselDots01() {
const [api, setApi] = React.useState<CarouselApi>()
const [current, setCurrent] = React.useState(0)
const [count, setCount] = React.useState(0)
React.useEffect(() => {
if (!api) return
setCount(api.scrollSnapList().length)
setCurrent(api.selectedScrollSnap())
api.on("select", () => setCurrent(api.selectedScrollSnap()))
}, [api])
return (
<div className="w-full max-w-xs">
<Carousel setApi={setApi}>
<CarouselContent>
{Array.from({ length: 5 }).map((_, i) => (
<CarouselItem key={i}>
<Card>
<CardContent className="flex aspect-square items-center justify-center p-6">
<span className="text-4xl font-semibold">{i + 1}</span>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
<div className="mt-4 flex items-center justify-center gap-2">
{Array.from({ length: count }).map((_, i) => (
<button
key={i}
onClick={() => api?.scrollTo(i)}
aria-label={`Go to slide ${i + 1}`}
className={cn(
"size-2 rounded-full transition-colors",
i === current ? "bg-primary" : "bg-primary/30"
)}
/>
))}
</div>
</div>
)
}
Autoplay
Auto-advancing slides with the `embla-carousel-autoplay` plugin, pausing on hover.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import Autoplay from "embla-carousel-autoplay"
import { Card, CardContent } from "@/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
export default function CarouselAutoplay01() {
const plugin = React.useRef(
Autoplay({ delay: 2000, stopOnInteraction: true })
)
return (
<Carousel
plugins={[plugin.current]}
className="w-full max-w-xs"
onMouseEnter={plugin.current.stop}
onMouseLeave={plugin.current.reset}
>
<CarouselContent>
{Array.from({ length: 5 }).map((_, i) => (
<CarouselItem key={i}>
<Card>
<CardContent className="flex aspect-square items-center justify-center p-6">
<span className="text-4xl font-semibold">{i + 1}</span>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}
Image carousel
A looping image carousel — aspect-video photos that slide with the arrows.
Packages
Props
No props documented yet.
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
const images = [10, 20, 30, 40, 50]
export default function CarouselImages01() {
return (
<Carousel opts={{ loop: true }} className="w-full max-w-md">
<CarouselContent>
{images.map((seed, i) => (
<CarouselItem key={seed}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`https://picsum.photos/seed/${seed}/640/360`}
alt={`Slide ${i + 1}`}
className="aspect-video w-full rounded-lg object-cover"
/>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}
Product carousel
Image cards with name and price, two per view — the e-commerce pattern.
Packages
Props
No props documented yet.
import { Card, CardContent } from "@/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
const products = [
{ id: 101, name: "Aergo Headphones", price: "$129" },
{ id: 102, name: "Lumen Desk Lamp", price: "$59" },
{ id: 103, name: "Terra Mug", price: "$24" },
{ id: 104, name: "Nimbus Backpack", price: "$89" },
{ id: 105, name: "Pulse Watch", price: "$199" },
]
export default function CarouselProduct01() {
return (
<Carousel opts={{ align: "start" }} className="w-full max-w-md">
<CarouselContent>
{products.map((product) => (
<CarouselItem key={product.id} className="basis-1/2">
<Card className="overflow-hidden p-0">
<CardContent className="p-0">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`https://picsum.photos/seed/${product.id}/400/400`}
alt={product.name}
className="aspect-square w-full object-cover"
/>
<div className="space-y-1 p-3">
<h3 className="text-sm leading-none font-medium">
{product.name}
</h3>
<p className="text-muted-foreground text-sm">
{product.price}
</p>
</div>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}
Thumbnails
A main image carousel synced to a thumbnail strip — two Embla instances driving each other.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel"
const images = [11, 22, 33, 44, 55]
export default function CarouselThumbnails01() {
const [mainApi, setMainApi] = React.useState<CarouselApi>()
const [thumbApi, setThumbApi] = React.useState<CarouselApi>()
const [current, setCurrent] = React.useState(0)
const onThumbClick = React.useCallback(
(index: number) => mainApi?.scrollTo(index),
[mainApi]
)
React.useEffect(() => {
if (!mainApi) return
const onSelect = () => {
const index = mainApi.selectedScrollSnap()
setCurrent(index)
thumbApi?.scrollTo(index)
}
onSelect()
mainApi.on("select", onSelect)
return () => {
mainApi.off("select", onSelect)
}
}, [mainApi, thumbApi])
return (
<div className="w-full max-w-md space-y-3">
<Carousel setApi={setMainApi} opts={{ loop: true }}>
<CarouselContent>
{images.map((seed, i) => (
<CarouselItem key={seed}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`https://picsum.photos/seed/${seed}/640/360`}
alt={`Slide ${i + 1}`}
className="aspect-video w-full rounded-lg object-cover"
/>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
<Carousel
setApi={setThumbApi}
opts={{ containScroll: "keepSnaps", dragFree: true }}
>
<CarouselContent className="-ml-2">
{images.map((seed, i) => (
<CarouselItem key={seed} className="basis-1/5 pl-2">
<button
onClick={() => onThumbClick(i)}
className={cn(
"overflow-hidden rounded-md border-2 transition-colors",
i === current ? "border-primary" : "border-transparent"
)}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`https://picsum.photos/seed/${seed}/120/80`}
alt={`Thumbnail ${i + 1}`}
className="aspect-video w-full object-cover"
/>
</button>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</div>
)
}
The Shadcn Carousel is a slider built on Embla — the lightweight, dependency-free carousel engine. You compose it from a few parts, and Embla handles the dragging, snapping, and momentum. The examples below go well beyond the basics: image and product carousels, multiple-per-view, vertical, dot indicators, autoplay, and a synced thumbnail strip.
Built on Embla
Install the engine:
npm install embla-carousel-react
Then compose the carousel:
<Carousel>
<CarouselContent>
<CarouselItem>…</CarouselItem>
<CarouselItem>…</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
Each CarouselItem is full-width by default. Pass Embla options through opts (e.g. { loop: true, align: "start" }) and Embla plugins through plugins.
Image carousel
The most-searched use — put an image in each slide and let it loop:
<Carousel opts={{ loop: true }}>
<CarouselContent>
{images.map((src) => (
<CarouselItem key={src}>
<img src={src} alt="" className="aspect-video w-full rounded-lg object-cover" />
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
The Image carousel example is a ready-to-use looping photo slider, and Thumbnails pairs the main image with a clickable thumbnail strip.
Multiple slides per view
Set a basis fraction on each item — basis-1/3 shows three at a time, basis-1/2 shows two. Add responsive variants to change the count by breakpoint:
<CarouselItem className="basis-1/2 md:basis-1/3">…</CarouselItem>
The Product carousel uses this for an e-commerce row of cards.
Vertical
Set orientation="vertical" and give the content a height:
<Carousel orientation="vertical">
<CarouselContent className="h-[260px]">…</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
The arrows automatically move to the top and bottom.
Dots & the API
Pass setApi to get Embla's API, then drive a dot row from it:
const [api, setApi] = React.useState<CarouselApi>()
React.useEffect(() => {
if (!api) return
setCount(api.scrollSnapList().length)
api.on("select", () => setCurrent(api.selectedScrollSnap()))
}, [api])
Render a dot per snap and call api.scrollTo(i) on click — the Dot indicators example.
Autoplay
Install embla-carousel-autoplay and pass it as a plugin, pausing on hover:
const plugin = React.useRef(Autoplay({ delay: 2000 }))
<Carousel
plugins={[plugin.current]}
onMouseEnter={plugin.current.stop}
onMouseLeave={plugin.current.reset}
>
Accessibility
The carousel is a labelled region (role="region", aria-roledescription="carousel") and each slide is a group with aria-roledescription="slide". The arrow buttons carry screen-reader labels, and Left / Right arrow keys scroll it when focused. Provide meaningful alt text on slide images.