# 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.

Source: https://designrevision.com/components/carousel

The **Shadcn Carousel** is a slider built on <a href="https://www.embla-carousel.com/" rel="nofollow">**Embla**</a> — 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:

```bash
npm install embla-carousel-react
```

Then compose the carousel:

```tsx
<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:

```tsx
<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:

```tsx
<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:

```tsx
<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:

```tsx
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:

```tsx
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.

## Installation

### carousel

**Install:**

```bash
npx shadcn@latest add @designrevision/carousel
```

**Dependencies:** embla-carousel-react, utils, button

**Props**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| orientation | "horizontal" \| "vertical" | "horizontal" | 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). |

**Usage & accessibility:** 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.

```tsx
// components/ui/carousel.tsx
"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.

**Install:**

```bash
npx shadcn@latest add @designrevision/carousel-demo-01
```

**Dependencies:** carousel, card

```tsx
// components/ui/carousel-demo-01.tsx
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"`.

**Install:**

```bash
npx shadcn@latest add @designrevision/carousel-sizes-01
```

**Dependencies:** carousel, card

```tsx
// components/ui/carousel-sizes-01.tsx
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.

**Install:**

```bash
npx shadcn@latest add @designrevision/carousel-vertical-01
```

**Dependencies:** carousel, card

```tsx
// components/ui/carousel-vertical-01.tsx
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`.

**Install:**

```bash
npx shadcn@latest add @designrevision/carousel-dots-01
```

**Dependencies:** carousel, card

```tsx
// components/ui/carousel-dots-01.tsx
"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.

**Install:**

```bash
npx shadcn@latest add @designrevision/carousel-autoplay-01
```

**Dependencies:** embla-carousel-autoplay, carousel, card

```tsx
// components/ui/carousel-autoplay-01.tsx
"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.

**Install:**

```bash
npx shadcn@latest add @designrevision/carousel-images-01
```

**Dependencies:** carousel

```tsx
// components/ui/carousel-images-01.tsx
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.

**Install:**

```bash
npx shadcn@latest add @designrevision/carousel-product-01
```

**Dependencies:** carousel, card

```tsx
// components/ui/carousel-product-01.tsx
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.

**Install:**

```bash
npx shadcn@latest add @designrevision/carousel-thumbnails-01
```

**Dependencies:** carousel

```tsx
// components/ui/carousel-thumbnails-01.tsx
"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>
  )
}
```
