---
id: "packages/active-ui/carousel"
title: "Carousel"
description: "Slides that scroll sideways one at a time, by buttons, swiping or the arrow keys, built on Embla."
url: "/packages/active-ui/carousel"
locale: "en"
revision: "0cf4b4b7dcecdd5d"
---

# Carousel

A carousel shows one slide of a series at a time and scrolls to the next by buttons, a swipe or
the arrow keys. Use it sparingly: content on a later slide is seen by few readers. It is built on
[Embla Carousel](https://www.embla-carousel.com), whose options and plugins it passes through.

**Example: News items, with the position as text**

Client: Runs entirely in your browser. Source: `examples/ui/carousel.tsx`.

```tsx
// apps/docs/examples/ui/carousel.tsx
'use client';

import {useEffect, useState} from 'react';

import {
  Card,
  CardContent,
} from '@workspace/active-ui/components/primitives/card';
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
  type CarouselApi,
} from '@workspace/active-ui/components/primitives/carousel';

const SLIDES = [
  'Statements go out on 1 November',
  'New: the participant portal in German',
  'Rates for 2027 are published',
  'The annual report 2025 is available',
];

/**
 * Slides that scroll sideways, one at a time: by the buttons, by swiping,
 * or with the arrow keys while the carousel has focus. `setApi` hands over
 * Embla's API, here to show the position as text.
 */
export default function CarouselExample() {
  const [api, setApi] = useState<CarouselApi>();
  const [current, setCurrent] = useState(1);
  useEffect(() => {
    if (!api) return;
    const update = () => setCurrent(api.selectedScrollSnap() + 1);
    update();
    api.on('select', update);
    return () => {
      api.off('select', update);
    };
  }, [api]);
  return (
    <div className="flex w-full flex-col items-center gap-3 px-12">
      <Carousel
        setApi={setApi}
        className="w-full max-w-xs"
        aria-label="News"
        roleDescription="carousel"
        slideRoleDescription="slide"
      >
        <CarouselContent>
          {SLIDES.map((text, index) => (
            <CarouselItem
              key={text}
              aria-label={`${index + 1} of ${SLIDES.length}`}
            >
              <Card>
                <CardContent className="flex aspect-[3/2] items-center justify-center text-center font-medium">
                  {text}
                </CardContent>
              </Card>
            </CarouselItem>
          ))}
        </CarouselContent>
        <CarouselPrevious label="Previous news item" />
        <CarouselNext label="Next news item" />
      </Carousel>
      <p className="text-muted-foreground text-sm" aria-live="polite">
        Item {current} of {SLIDES.length}
      </p>
    </div>
  );
}
```

## Usage

```tsx
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from '@workspace/active-ui/components/primitives/carousel';

<Carousel
  aria-label="News"
  roleDescription="carousel"
  slideRoleDescription="slide"
>
  <CarouselContent>
    <CarouselItem>…</CarouselItem>
    <CarouselItem>…</CarouselItem>
  </CarouselContent>
  <CarouselPrevious label="Previous news item" />
  <CarouselNext label="Next news item" />
</Carousel>;
```

## Anatomy

- `Carousel`: the region, `role="region"`. `orientation` (`horizontal` or `vertical`), `opts`
  (Embla's options, such as `loop` or `align`), `plugins` (such as autoplay), and `setApi`, which
  receives Embla's API to read or move the position.
- `CarouselContent`: the track. `CarouselItem`: one slide, full width by default; set a `basis`
  class (`basis-1/2`) to show several.
- `CarouselPrevious` and `CarouselNext`: round buttons outside the track (`-left-12` and
  `-right-12`), disabled at the ends unless the carousel loops. Leave room for them.
- `useCarousel()`: the carousel's context for a control of your own (`scrollPrev`, `scrollNext`,
  `canScrollPrev`, `canScrollNext`, `api`).

## What your application provides

- A name for the region (`aria-label`), and the words a screen reader says for the region and for
  a slide (`roleDescription`, `slideRoleDescription`), in your reader's language (required).
- `label` of the previous and next buttons (required), and a name per slide when the position
  matters, such as "2 of 4".
- The position as visible text when it helps, as in the example.
- If you add autoplay: a way to pause it.

## Accessibility and keyboard

- `←` and `→` scroll while the focus is inside the carousel.
- The buttons are disabled at the ends, so the keyboard cannot move past the first or last slide.
- Slides are `role="group"` with the slide role description.

## API

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `roleDescription` | `string` | yes | What a screen reader calls this region instead of "regio": "carrousel".  Supplied already translated, and required: `aria-roledescription` is spoken literally and is therefore user-visible text, not a technical value. |
| `slideRoleDescription` | `string` | yes | Same for a single item; `CarouselItem` reads it from the context. |
| `opts` | `Partial<OptionsType>` | no | Embla's options, such as `loop` or `align`; the axis follows `orientation`. |
| `orientation` | `"horizontal" \| "vertical"` | no | The direction the slides scroll in. |
| `plugins` | `CreatePluginType<LoosePluginType, {}>[]` | no | Embla plugins, such as autoplay. |
| `setApi` | `((api: CarouselApi) => void)` | no | Receives Embla's API once it is ready, to read or move the position. |

### Other parts

**CarouselContent**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

CarouselContent has no props of its own.

**CarouselItem**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

CarouselItem has no props of its own.

**CarouselPrevious**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `string` | yes | The accessible name of the button, such as "Next slide". |
| `size` | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "lg" \| "sm" \| "xs" \| null` | no | The height and padding. `xs`, `sm`, `default` and `lg` for a button with text; `icon-xs`, `icon-sm`, `icon` and `icon-lg` for a square button with only an icon, which then needs an accessible name (`aria-label`). |
| `variant` | `"default" \| "destructive" \| "ghost" \| "link" \| "outline" \| "secondary" \| null` | no | The visual weight of the button. `default` for the main action of a screen or form; `outline` and `secondary` for other actions; `ghost` for a quiet action in a toolbar or a list; `destructive` for an action that deletes or cannot be undone; `link` for an action that reads as a link. Every variant but `link` sinks 1px while pressed; a link-styled action keeps its text and icon in place (`button-link-still`). |
| `focusableWhenDisabled` | `boolean` | no | Whether the button should be focusable when disabled. |
| `nativeButton` | `boolean` | no | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| `render` | `ComponentRenderFn<HTMLProps, ButtonState> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

**CarouselNext**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `string` | yes | The accessible name of the button, such as "Next slide". |
| `size` | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "lg" \| "sm" \| "xs" \| null` | no | The height and padding. `xs`, `sm`, `default` and `lg` for a button with text; `icon-xs`, `icon-sm`, `icon` and `icon-lg` for a square button with only an icon, which then needs an accessible name (`aria-label`). |
| `variant` | `"default" \| "destructive" \| "ghost" \| "link" \| "outline" \| "secondary" \| null` | no | The visual weight of the button. `default` for the main action of a screen or form; `outline` and `secondary` for other actions; `ghost` for a quiet action in a toolbar or a list; `destructive` for an action that deletes or cannot be undone; `link` for an action that reads as a link. Every variant but `link` sinks 1px while pressed; a link-styled action keeps its text and icon in place (`button-link-still`). |
| `focusableWhenDisabled` | `boolean` | no | Whether the button should be focusable when disabled. |
| `nativeButton` | `boolean` | no | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| `render` | `ComponentRenderFn<HTMLProps, ButtonState> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

**useCarousel**: `() => CarouselContextProps`

The carousel's context, for a control of your own inside it:
`scrollPrev`, `scrollNext`, `canScrollPrev`, `canScrollNext`, `api` and
the `orientation`. Throws outside a `Carousel`.

Returns an object with:

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `opts` | `Partial<OptionsType>` | no | Embla's options, such as `loop` or `align`; the axis follows `orientation`. |
| `plugins` | `CreatePluginType<LoosePluginType, {}>[]` | no | Embla plugins, such as autoplay. |
| `orientation` | `"horizontal" \| "vertical"` | no | The direction the slides scroll in. |
| `setApi` | `((api: CarouselApi) => void)` | no | Receives Embla's API once it is ready, to read or move the position. |
| `roleDescription` | `string` | yes | What a screen reader calls this region instead of "regio": "carrousel".  Supplied already translated, and required: `aria-roledescription` is spoken literally and is therefore user-visible text, not a technical value. |
| `slideRoleDescription` | `string` | yes | Same for a single item; `CarouselItem` reads it from the context. |
| `carouselRef` | `EmblaViewportRefType` | yes |  |
| `api` | `EmblaCarouselType \| undefined` | yes |  |
| `scrollPrev` | `() => void` | yes |  |
| `scrollNext` | `() => void` | yes |  |
| `canScrollPrev` | `boolean` | yes |  |
| `canScrollNext` | `boolean` | yes |  |

**CarouselApi**

```ts
type CarouselApi = UseEmblaCarouselType[1];
```

> [!NOTE]
> **Based on shadcn/ui**
>
> This page adapts the Carousel documentation of shadcn/ui (MIT licence) to this
> package; see [Third-party notices](/third-party-notices).
