# Carousel

A carousel with motion and swipe, built on [Embla Carousel](https://www.embla-carousel.com/).

```twig
<twig:Carousel class="mx-auto w-full max-w-[10rem] sm:max-w-[14rem]">
    <twig:Carousel:Content>
        {% for i in 1..5 %}
            <twig:Carousel:Item>
                <div class="p-1">
                    <twig:Card>
                        <twig:Card:Content class="flex aspect-square items-center justify-center p-6">
                            <span class="text-4xl font-semibold">{{ i }}</span>
                        </twig:Card:Content>
                    </twig:Card>
                </div>
            </twig:Carousel:Item>
        {% endfor %}
    </twig:Carousel:Content>
    <twig:Carousel:Previous />
    <twig:Carousel:Next />
</twig:Carousel>
```

The carousel is focusable and responds to arrow keys: `Left`/`Right` on a horizontal carousel, `Up`/`Down` on a vertical one. In RTL, the horizontal mapping is mirrored.

## Installation

> [!NOTE]
> Available since UX Toolkit 3.5.

```shell
php bin/console ux:install carousel --kit shadcn
```
## Usage

```twig
<twig:Carousel orientation="horizontal | vertical" align="start | center | end" loop>
    <twig:Carousel:Content>
        <twig:Carousel:Item>...</twig:Carousel:Item>
        <twig:Carousel:Item>...</twig:Carousel:Item>
        <twig:Carousel:Item>...</twig:Carousel:Item>
    </twig:Carousel:Content>
    <twig:Carousel:Previous />
    <twig:Carousel:Next />
</twig:Carousel>
```

## Examples

### Sizes

Set slide size with a `basis-*` utility on `Carousel:Item`.

```twig
<twig:Carousel align="start" class="mx-auto w-full max-w-[12rem] sm:max-w-xs md:max-w-sm">
    <twig:Carousel:Content>
        {% for i in 1..5 %}
            <twig:Carousel:Item class="basis-1/3">
                <div class="p-1">
                    <twig:Card>
                        <twig:Card:Content class="flex aspect-square items-center justify-center p-6">
                            <span class="text-3xl font-semibold">{{ i }}</span>
                        </twig:Card:Content>
                    </twig:Card>
                </div>
            </twig:Carousel:Item>
        {% endfor %}
    </twig:Carousel:Content>
    <twig:Carousel:Previous />
    <twig:Carousel:Next />
</twig:Carousel>
```

### Spacing

Set spacing between slides with a `ps-*` utility on `Carousel:Item`, matched by a negative `-ms-*` on `Carousel:Content`.

```twig
<twig:Carousel class="mx-auto w-full max-w-[12rem] sm:max-w-xs md:max-w-sm">
    <twig:Carousel:Content class="-ms-1">
        {% for i in 1..5 %}
            <twig:Carousel:Item class="basis-1/3 ps-1">
                <div class="p-1">
                    <twig:Card>
                        <twig:Card:Content class="flex aspect-square items-center justify-center p-6">
                            <span class="text-2xl font-semibold">{{ i }}</span>
                        </twig:Card:Content>
                    </twig:Card>
                </div>
            </twig:Carousel:Item>
        {% endfor %}
    </twig:Carousel:Content>
    <twig:Carousel:Previous />
    <twig:Carousel:Next />
</twig:Carousel>
```

### Orientation

The `orientation` prop sets the axis the slides scroll along. A vertical carousel needs an explicit height on `Carousel:Content`.

```twig
<div class="w-full py-16">
    <twig:Carousel orientation="vertical" align="start" class="mx-auto w-full max-w-xs">
        <twig:Carousel:Content class="-mt-1 h-[270px]">
            {% for i in 1..5 %}
                <twig:Carousel:Item class="basis-1/2 pt-1">
                    <div class="p-1">
                        <twig:Card>
                            <twig:Card:Content class="flex items-center justify-center p-6">
                                <span class="text-3xl font-semibold">{{ i }}</span>
                            </twig:Card:Content>
                        </twig:Card>
                    </div>
                </twig:Carousel:Item>
            {% endfor %}
        </twig:Carousel:Content>
        <twig:Carousel:Previous />
        <twig:Carousel:Next />
    </twig:Carousel>
</div>
```

### Options

Use the `align` prop to set where the selected slide rests inside the viewport, that's the only difference between the three carousels below. Each one also passes `containScroll: false` through `options`, which forwards any [Embla Carousel option](https://www.embla-carousel.com/api/options/): without it, Embla trims the empty space at both ends and every alignment lands on the same position.

```twig
<div class="mx-auto flex w-full max-w-[10rem] flex-col gap-8 sm:max-w-xs">
    {% for alignment in ['start', 'center', 'end'] %}
        <div>
            <p class="mb-2 text-center text-sm text-muted-foreground">align="{{ alignment }}"</p>
            <twig:Carousel align="{{ alignment }}" :options="{containScroll: false}">
                <twig:Carousel:Content>
                    {% for i in 1..5 %}
                        <twig:Carousel:Item class="basis-1/3">
                            <div class="p-1">
                                <twig:Card>
                                    <twig:Card:Content class="flex aspect-square items-center justify-center p-2">
                                        <span class="text-xl font-semibold">{{ i }}</span>
                                    </twig:Card:Content>
                                </twig:Card>
                            </div>
                        </twig:Carousel:Item>
                    {% endfor %}
                </twig:Carousel:Content>
                <twig:Carousel:Previous />
                <twig:Carousel:Next />
            </twig:Carousel>
        </div>
    {% endfor %}
</div>
```

### Loop

The `loop` prop wraps around after the last slide, so `Carousel:Previous` stays enabled on the first one and `Carousel:Next` on the last.

```twig
<twig:Carousel align="start" loop class="mx-auto w-full max-w-[12rem] sm:max-w-xs md:max-w-sm">
    <twig:Carousel:Content>
        {% for i in 1..5 %}
            <twig:Carousel:Item class="basis-1/3">
                <div class="p-1">
                    <twig:Card>
                        <twig:Card:Content class="flex aspect-square items-center justify-center p-6">
                            <span class="text-3xl font-semibold">{{ i }}</span>
                        </twig:Card:Content>
                    </twig:Card>
                </div>
            </twig:Carousel:Item>
        {% endfor %}
    </twig:Carousel:Content>
    <twig:Carousel:Previous />
    <twig:Carousel:Next />
</twig:Carousel>
```

### API

The carousel dispatches a `carousel:select` event on every slide change, with the slide `index` and `count` in its detail. The `carousel-display` controller shipped with this recipe uses it to show the current position.

```twig
<div class="mx-auto w-full max-w-[10rem] sm:max-w-[14rem]" data-controller="carousel-display" data-action="carousel:select->carousel-display#update">
    <twig:Carousel>
        <twig:Carousel:Content>
            {% for i in 1..5 %}
                <twig:Carousel:Item>
                    <twig:Card class="m-px">
                        <twig:Card:Content class="flex aspect-square items-center justify-center p-6">
                            <span class="text-4xl font-semibold">{{ i }}</span>
                        </twig:Card:Content>
                    </twig:Card>
                </twig:Carousel:Item>
            {% endfor %}
        </twig:Carousel:Content>
        <twig:Carousel:Previous />
        <twig:Carousel:Next />
    </twig:Carousel>
    <div class="py-2 text-center text-sm text-muted-foreground" data-carousel-display-target="output">Slide 1 of 5</div>
</div>
```

### Autoplay

The `autoplay` prop takes a delay in milliseconds, `0` disables it. It is backed by Embla's autoplay plugin, configured to pause while the pointer is over the carousel and to resume after the previous/next buttons are clicked.

```twig
<twig:Carousel autoplay="2000" loop class="mx-auto w-full max-w-[10rem] sm:max-w-[14rem]">
    <twig:Carousel:Content>
        {% for i in 1..5 %}
            <twig:Carousel:Item>
                <div class="p-1">
                    <twig:Card>
                        <twig:Card:Content class="flex aspect-square items-center justify-center p-6">
                            <span class="text-4xl font-semibold">{{ i }}</span>
                        </twig:Card:Content>
                    </twig:Card>
                </div>
            </twig:Carousel:Item>
        {% endfor %}
    </twig:Carousel:Content>
    <twig:Carousel:Previous />
    <twig:Carousel:Next />
</twig:Carousel>
```

### RTL

To enable RTL support, set the `dir="rtl"` attribute on the root element.

```twig
{% set arabicNumerals = ['١', '٢', '٣', '٤', '٥'] %}
<div class="flex flex-col items-center gap-12">
    {# Arabic #}
    <twig:Carousel dir="rtl" class="w-full max-w-[10rem] sm:max-w-[14rem]">
        <twig:Carousel:Content>
            {% for i in 1..5 %}
                <twig:Carousel:Item>
                    <div class="p-1">
                        <twig:Card>
                            <twig:Card:Content class="flex aspect-square items-center justify-center p-6">
                                <span class="text-4xl font-semibold">{{ arabicNumerals[i - 1] }}</span>
                            </twig:Card:Content>
                        </twig:Card>
                    </div>
                </twig:Carousel:Item>
            {% endfor %}
        </twig:Carousel:Content>
        <twig:Carousel:Previous text="الشريحة السابقة" />
        <twig:Carousel:Next text="الشريحة التالية" />
    </twig:Carousel>

    {# Hebrew #}
    <twig:Carousel dir="rtl" class="w-full max-w-[10rem] sm:max-w-[14rem]">
        <twig:Carousel:Content>
            {% for i in 1..5 %}
                <twig:Carousel:Item>
                    <div class="p-1">
                        <twig:Card>
                            <twig:Card:Content class="flex aspect-square items-center justify-center p-6">
                                <span class="text-4xl font-semibold">{{ i }}</span>
                            </twig:Card:Content>
                        </twig:Card>
                    </div>
                </twig:Carousel:Item>
            {% endfor %}
        </twig:Carousel:Content>
        <twig:Carousel:Previous text="השקופית הקודמת" />
        <twig:Carousel:Next text="השקופית הבאה" />
    </twig:Carousel>
</div>
```

## Accessibility

- `Carousel` renders `role="region"` with `aria-roledescription="carousel"` and is focusable, so a screen reader user can reach it and be told what kind of widget it is. Set `label` to name it, since the default is just `Carousel`.
- Each `Carousel:Item` carries `role="group"` and `aria-roledescription="slide"`.
- With the carousel focused, arrow keys move between slides: left and right when `orientation` is `horizontal`, up and down when it is `vertical`.
- `Carousel:Previous` and `Carousel:Next` are icon-only buttons that carry a visually hidden label, controlled by their `text` prop. Translate it rather than dropping it.
- Autoplay moves content without the user asking. Keep the `autoplay` delay long enough to read a slide, and prefer leaving it at `0` when the slides carry essential content.

## API Reference

### `<twig:Carousel>`

| Prop | Type | Default | Description |
|:-----|:-----|:--------|:------------|
| `orientation` | `'horizontal'\|'vertical'` | `'horizontal'` | The axis the slides scroll along. |
| `align` | `'start'\|'center'\|'end'` | `'center'` | The alignment of the selected slide inside the viewport. |
| `loop` | `boolean` | `false` | Whether the carousel wraps around after the last slide. |
| `options` | `array` | `[]` | Extra [Embla Carousel options](https://www.embla-carousel.com/api/options/), merged over `align` and `loop`. |
| `autoplay` | `int` | `0` | The delay in milliseconds between two automatic slide changes. Zero disables autoplay. |
| `label` | `string` | `'Carousel'` | The accessible name of the carousel region. |

| Block | Description |
|:------|:------------|
| `content` | The carousel structure, typically includes `Carousel:Content`, `Carousel:Previous` and `Carousel:Next`. |
### `<twig:Carousel:Content>`

| Block | Description |
|:------|:------------|
| `content` | The carousel slides, typically multiple `Carousel:Item` components. |
### `<twig:Carousel:Item>`

| Block | Description |
|:------|:------------|
| `content` | The slide content. |
### `<twig:Carousel:Next>`

| Prop | Type | Default | Description |
|:-----|:-----|:--------|:------------|
| `variant` | `'default'\|'secondary'\|'destructive'\|'outline'\|'ghost'\|'link'` | `'outline'` | The button style variant. |
| `size` | `'default'\|'xs'\|'sm'\|'lg'\|'icon'\|'icon-xs'\|'icon-sm'\|'icon-lg'` | `'icon-sm'` | The button size. |
| `text` | `string` | `'Next slide'` | The accessible label of the button. |
### `<twig:Carousel:Previous>`

| Prop | Type | Default | Description |
|:-----|:-----|:--------|:------------|
| `variant` | `'default'\|'secondary'\|'destructive'\|'outline'\|'ghost'\|'link'` | `'outline'` | The button style variant. |
| `size` | `'default'\|'xs'\|'sm'\|'lg'\|'icon'\|'icon-xs'\|'icon-sm'\|'icon-lg'` | `'icon-sm'` | The button size. |
| `text` | `string` | `'Previous slide'` | The accessible label of the button. |
### `data-controller="carousel-display"`

| Value | Type | Default | Description |
|:------|:-----|:--------|:------------|
| `data-carousel-display-template-value` | `String` | `'Slide %index% of %count%'` | The output text, where `%index%` and `%count%` are replaced by the current slide number and the total number of slides. |

| Target | Description |
|:-------|:------------|
| `output` | The element whose text content displays the carousel position. |

| Action | Description |
|:-------|:------------|
| `update` | Writes the carousel's current position into the output target. |