---
title: Carousel
description: Horizontal or vertical slides with controls and swipe support.
group: Components
order: 23
---

Build swipeable galleries on Embla. Compose the slides, then place previous and next controls beside the track or in a row below it.

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

```bash
npx @arctis-sh/@arctis-sh/ui@latest add carousel
```

Installs with `Button` and requires the `embla-carousel-react` peer dependency. The plugins example also uses `embla-carousel-autoplay`.

## Usage

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

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

```tree
Carousel
├── CarouselContent
│   ├── CarouselItem
│   └── CarouselItem
├── CarouselPrevious
├── CarouselNext
├── CarouselControls
└── CarouselStatus
```

## Controls

`CarouselControls` places previous and next controls below the track.

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

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

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

```tsx
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>
  )
}
```

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

```tsx
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>
  )
}
```

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

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

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

```tsx
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>
  )
}
```

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

```tsx
useEffect(() => {
  if (!api) return

  api.on("select", () => {
    // Do something on select.
  })
}, [api])
```

## Plugins

Pass Embla plugins through the `plugins` prop.

```tsx
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>
  )
}
```

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

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

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

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

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

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

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

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

```tsx
const [api, setApi] = useState<CarouselApi>()

<Carousel setApi={setApi}>
  ...
</Carousel>
```

## Source

```tsx
"use client";

import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useState,
  type ComponentProps,
  type HTMLAttributes,
  type KeyboardEvent,
} from "react";
import useEmblaCarousel, {
  type UseEmblaCarouselType,
} from "embla-carousel-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

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 = createContext<CarouselContextProps | null>(null);

function useCarousel() {
  const context = useContext(CarouselContext);
  if (!context) {
    throw new Error("useCarousel must be used within a <Carousel />");
  }
  return context;
}

function ArrowLeftIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={cn("size-4", className)}
    >
      <path d="m12 19-7-7 7-7" />
      <path d="M19 12H5" />
    </svg>
  );
}

function ArrowRightIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={cn("size-4", className)}
    >
      <path d="M5 12h14" />
      <path d="m12 5 7 7-7 7" />
    </svg>
  );
}

function Carousel({
  orientation = "horizontal",
  opts,
  setApi,
  plugins,
  className,
  children,
  ...props
}: HTMLAttributes<HTMLDivElement> & CarouselProps) {
  const [carouselRef, api] = useEmblaCarousel(
    {
      ...opts,
      axis: orientation === "horizontal" ? "x" : "y",
    },
    plugins,
  );
  const [canScrollPrev, setCanScrollPrev] = useState(false);
  const [canScrollNext, setCanScrollNext] = useState(false);

  const onSelect = useCallback((carouselApi: CarouselApi) => {
    if (!carouselApi) return;
    setCanScrollPrev(carouselApi.canScrollPrev());
    setCanScrollNext(carouselApi.canScrollNext());
  }, []);

  const scrollPrev = useCallback(() => {
    api?.scrollPrev();
  }, [api]);

  const scrollNext = useCallback(() => {
    api?.scrollNext();
  }, [api]);

  const handleKeyDown = useCallback(
    (event: KeyboardEvent<HTMLDivElement>) => {
      if (event.key === "ArrowLeft") {
        event.preventDefault();
        scrollPrev();
      } else if (event.key === "ArrowRight") {
        event.preventDefault();
        scrollNext();
      }
    },
    [scrollPrev, scrollNext],
  );

  useEffect(() => {
    if (!api || !setApi) return;
    setApi(api);
  }, [api, setApi]);

  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,
        opts,
        orientation:
          orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
        scrollPrev,
        scrollNext,
        canScrollPrev,
        canScrollNext,
      }}
    >
      <div
        data-slot="carousel"
        onKeyDownCapture={handleKeyDown}
        className={cn("relative", className)}
        role="region"
        aria-roledescription="carousel"
        {...props}
      >
        {children}
      </div>
    </CarouselContext.Provider>
  );
}

function CarouselContent({
  className,
  ...props
}: HTMLAttributes<HTMLDivElement>) {
  const { carouselRef, orientation } = useCarousel();

  return (
    <div
      ref={carouselRef}
      data-slot="carousel-content"
      className={cn(
        "overflow-hidden",
        orientation === "vertical" && className,
      )}
    >
      <div
        className={cn(
          "flex",
          orientation === "horizontal"
            ? cn("-ml-4", className)
            : "h-full flex-col",
        )}
        {...props}
      />
    </div>
  );
}

function CarouselItem({
  className,
  ...props
}: HTMLAttributes<HTMLDivElement>) {
  const { orientation } = useCarousel();

  return (
    <div
      data-slot="carousel-item"
      role="group"
      aria-roledescription="slide"
      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-sm",
  ...props
}: ComponentProps<typeof Button>) {
  const { orientation, scrollPrev, canScrollPrev } = useCarousel();

  return (
    <Button
      data-slot="carousel-previous"
      variant={variant}
      size={size}
      className={cn(orientation === "vertical" && "rotate-90", className)}
      disabled={!canScrollPrev}
      onClick={scrollPrev}
      aria-label="Previous slide"
      {...props}
    >
      <ArrowLeftIcon />
    </Button>
  );
}

function CarouselNext({
  className,
  variant = "outline",
  size = "icon-sm",
  ...props
}: ComponentProps<typeof Button>) {
  const { orientation, scrollNext, canScrollNext } = useCarousel();

  return (
    <Button
      data-slot="carousel-next"
      variant={variant}
      size={size}
      className={cn(orientation === "vertical" && "rotate-90", className)}
      disabled={!canScrollNext}
      onClick={scrollNext}
      aria-label="Next slide"
      {...props}
    >
      <ArrowRightIcon />
    </Button>
  );
}

function CarouselStatus({
  className,
  ...props
}: HTMLAttributes<HTMLDivElement>) {
  const { api } = useCarousel();
  const [current, setCurrent] = useState(0);
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (!api) return;

    const update = () => {
      setCount(api.scrollSnapList().length);
      setCurrent(api.selectedScrollSnap() + 1);
    };

    update();
    api.on("reInit", update);
    api.on("select", update);
    return () => {
      api.off("reInit", update);
      api.off("select", update);
    };
  }, [api]);

  return (
    <div
      data-slot="carousel-status"
      className={cn("text-sm text-muted-foreground tabular-nums", className)}
      {...props}
    >
      Slide {current} of {count}
    </div>
  );
}

function CarouselControls({
  showStatus = false,
  className,
  ...props
}: HTMLAttributes<HTMLDivElement> & {
  showStatus?: boolean;
}) {
  return (
    <div
      data-slot="carousel-controls"
      className={cn(
        "mt-4 flex items-center justify-between gap-4",
        className,
      )}
      {...props}
    >
      <CarouselPrevious />
      {showStatus ? <CarouselStatus /> : null}
      <CarouselNext />
    </div>
  );
}

export {
  type CarouselApi,
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselPrevious,
  CarouselNext,
  CarouselStatus,
  CarouselControls,
};
```
