---
type: doc
title: Carousel
description: A carousel with motion, swipe and responsive slides built using Embla.
---

> Fulldev UI for Astro. The `@fulldev` registry in `components.json` points at `https://ui.full.dev/r/styles/{style}/{name}.json`; see [installation](/astro/docs/installation.md).

```astro title="src/components/examples/carousel/carousel-demo.astro"
---
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

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

## Usage

```ts
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel"
```

```astro
<Carousel aria-label="Featured slides">
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>
```

The carousel runs the vanilla [Embla Carousel](https://www.embla-carousel.com/)
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](https://www.w3.org/WAI/ARIA/apg/patterns/carousel/)
recommends; without a name it gets `role="group"`.

## Composition

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

```astro title="src/components/examples/carousel/carousel-size.astro"
---
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`.

```astro title="src/components/examples/carousel/carousel-spacing.astro"
---
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.

```astro title="src/components/examples/carousel/carousel-orientation.astro"
---
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](https://www.embla-carousel.com/api/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.

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

```astro title="src/components/examples/carousel/carousel-api.astro"
---
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`.

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

```astro title="src/components/examples/carousel/carousel-plugin.astro"
---
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.

```astro title="src/components/examples/carousel/carousel-rtl.astro"
---
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`](https://github.com/fulldotdev/ui/tree/main/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                          |

## Install what this page shows

```bash
npx shadcn@latest add @fulldev/carousel-examples
```
