---
title: "Carousel"
description: "Carousels with pagination, navigation and thumbs."
requested_language: sk
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/sk/next/components/carousel
version: unreleased (main)
docs_index: https://docs.systhema.app/sk/next/llms.txt
---
> This page isn't translated yet. Showing English.


`Carousel` shows one slide at a time, with optional arrows and pagination over the slides or in a bar below them, and an optional strip of thumbnails. It is a [`Figure`](https://docs.systhema.app/sk/next/components/figure.md), so it takes the same `width` breakouts. The slider is [Swiper](https://swiperjs.com/), loaded only on pages that have a carousel.

```tsx preview iframe height=520 title="Carousel"
import { Carousel, CarouselItem, Image } from '@systhemaui/next'

const slides = [
  { src: '/demo-assets/coast.webp', alt: 'A rocky coastline at dusk' },
  { src: '/demo-assets/workspace.webp', alt: 'A tidy desk with a laptop' },
  { src: '/demo-assets/abstract-square.webp', alt: 'Soft abstract shapes' },
]

export default function Demo() {
  return (
    <div className="w-full max-w-3xl">
      <Carousel loop pagination="wrapper" navigation="wrapper">
        {slides.map((slide) => (
          <CarouselItem key={slide.src}>
            <Image src={slide.src} alt={slide.alt} width={1800} height={1200} aspectRatio="16/9" />
          </CarouselItem>
        ))}
      </Carousel>
    </div>
  )
}
```

## Import

```tsx
import { Carousel, CarouselItem } from '@systhemaui/next'
```

In a React app without Next.js, import them from `@systhemaui/react`. The API is the same.

## Variants and tags

`Carousel` renders a `<figure class="carousel">`, and each `CarouselItem` becomes one full-width slide. Three props shape the controls:

| Prop         | `'none'` (default) | `'wrapper'`                                    | `'bottom'`                                    |
| ------------ | ------------------ | ---------------------------------------------- | --------------------------------------------- |
| `navigation` | no arrows          | arrows over the slide, at its left and right   | arrows in a bar under the slide               |
| `pagination` | no pagination      | clickable bullets over the bottom of the slide | a `1 / 3` fraction in the bar under the slide |

`thumbs` adds a strip of clickable thumbnails under the slide, made from the same children. `loop` wraps from the last slide to the first; it only applies with two or more slides.

## Examples

### Controls below the slide

With `'bottom'`, the fraction and the arrows sit in a bar under the slide instead of on top of it, which keeps the image unobstructed.

```tsx preview iframe height=560 title="Bottom bar"
import { Carousel, CarouselItem, Image } from '@systhemaui/next'

const slides = ['/demo-assets/coast.webp', '/demo-assets/workspace.webp']

export default function Demo() {
  return (
    <div className="w-full max-w-3xl">
      <Carousel pagination="bottom" navigation="bottom">
        {slides.map((src) => (
          <CarouselItem key={src}>
            <Image src={src} alt="" width={1800} height={1200} aspectRatio="16/9" />
          </CarouselItem>
        ))}
      </Carousel>
    </div>
  )
}
```

### Thumbnails

`thumbs` renders the slides again as square thumbnails under the main slider. The active one is fully opaque, the others dimmed; click one to show it.

```tsx preview iframe height=600 title="Thumbs"
import { Carousel, CarouselItem, Image } from '@systhemaui/next'

const slides = [
  { src: '/demo-assets/coast.webp', width: 1800, height: 1200 },
  { src: '/demo-assets/dunes-portrait.webp', width: 1200, height: 1600 },
  { src: '/demo-assets/workspace.webp', width: 1800, height: 1200 },
  { src: '/demo-assets/forest-portrait.webp', width: 1200, height: 1600 },
  { src: '/demo-assets/mountains.webp', width: 1920, height: 1080 },
]

export default function Demo() {
  return (
    <div className="w-full max-w-3xl">
      <Carousel navigation="wrapper" thumbs>
        {slides.map((slide) => (
          <CarouselItem key={slide.src}>
            <Image
              src={slide.src}
              alt=""
              width={slide.width}
              height={slide.height}
              aspectRatio="16/9"
            />
          </CarouselItem>
        ))}
      </Carousel>
    </div>
  )
}
```

### Full-bleed width

`width` works as on [`Figure`](https://docs.systhema.app/sk/next/components/figure.md#width-and-breakout-behaviour): `'default'` fills the column, `'container'` breaks out to the container width, and `'screen'` to the full viewport, without radius or shadow. With `'screen'`, the thumbnails and the bottom bar are not rendered, so use `'wrapper'` controls.

```tsx preview iframe bleed height=520 title="Screen width"
import { Carousel, CarouselItem, Image } from '@systhemaui/next'

export default function Demo() {
  return (
    <Carousel width="screen" loop pagination="wrapper" navigation="wrapper">
      <CarouselItem>
        <Image src="/demo-assets/mountains.webp" alt="" width={1920} height={1080} aspectRatio="21/9" />
      </CarouselItem>
      <CarouselItem>
        <Image src="/demo-assets/city.webp" alt="" width={1920} height={1080} aspectRatio="21/9" />
      </CarouselItem>
    </Carousel>
  )
}
```

### Translated button labels

The arrows are labelled "Previous slide" and "Next slide". Pass `labels` to translate them.

```tsx
import { Carousel, CarouselItem, Image } from '@systhemaui/next'

export function Diashow({ photos }: { photos: { src: string; alt: string }[] }) {
  return (
    <Carousel
      navigation="wrapper"
      labels={{ previousSlide: 'Vorheriges Bild', nextSlide: 'Nächstes Bild' }}
    >
      {photos.map((photo) => (
        <CarouselItem key={photo.src}>
          <Image src={photo.src} alt={photo.alt} width={1800} height={1200} aspectRatio="16/9" />
        </CarouselItem>
      ))}
    </Carousel>
  )
}
```

## Loading

`Carousel` is a client component. Both Swiper instances (main slider and thumbnails) come from one lazily imported module in its own chunk, about 120 KB that pages without a carousel never fetch. Until it arrives, the slides render with the same `.swiper` markup, and the server render already contains the real markup. See [Swiper is loaded on demand](https://docs.systhema.app/sk/next/concepts/client-and-server.md#swiper-is-loaded-on-demand).

## Props

### `Carousel`

`width` defaults to `'default'`, `loop` and `thumbs` to `false`, `pagination` and `navigation` to `'none'`. `disableAnimation` drops the configured `animationClasses`.

<!-- generated:props @systhemaui/react CarouselProps -->

| Prop                               | Type                                                     | Default     | Description                                                                |
| ---------------------------------- | -------------------------------------------------------- | ----------- | -------------------------------------------------------------------------- |
| `children`                         | `ReactNode`                                              | -           |                                                                            |
| `width`                            | `'default' \| 'container' \| 'screen' \| 'card' \| null` | `'default'` |                                                                            |
| `className`                        | `string`                                                 | -           |                                                                            |
| `loop`                             | `boolean`                                                | `false`     |                                                                            |
| `pagination`                       | `'none' \| 'wrapper' \| 'bottom'`                        | `'none'`    |                                                                            |
| `navigation`                       | `'none' \| 'wrapper' \| 'bottom'`                        | `'none'`    |                                                                            |
| `thumbs`                           | `boolean`                                                | `false`     |                                                                            |
| `disableAnimation`                 | `boolean`                                                | `false`     |                                                                            |
| `labels`                           | `{ previousSlide?, nextSlide? }`                         | -           | Override the prev/next navigation button `aria-label`s (for localization). |
| …and all inherited HTML attributes |                                                          |             |                                                                            |

<!-- /generated -->

### `CarouselItem`

`CarouselItem` renders its child as a slide and takes no other props.

<!-- generated:props @systhemaui/react CarouselItemProps -->

| Prop                  | Type        | Default | Description |
| --------------------- | ----------- | ------- | ----------- |
| `children` (required) | `ReactNode` | -       |             |

<!-- /generated -->

## HTML and CSS

The carousel classes style the Swiper markup; the slider itself needs Swiper. A static version with arrows over the slide:

```html
<figure class="figure-w-default carousel" data-carousel-id="c1">
  <div class="carousel-wrapper">
    <div class="swiper" id="c1-swiper">
      <div class="swiper-wrapper">
        <div class="swiper-slide">
          <img class="media aspect-16/9 object-cover" src="/coast.webp" alt="" />
        </div>
      </div>
    </div>
    <div class="carousel-wrapper-pagination"></div>
    <button class="carousel-wrapper-button-prev" aria-controls="c1-swiper">
      <span class="sr-only!">Previous slide</span>
      <span class="carousel-wrapper-button-icon carousel-wrapper-button-icon-prev"></span>
    </button>
    <button class="carousel-wrapper-button-next" aria-controls="c1-swiper">
      <span class="sr-only!">Next slide</span>
      <span class="carousel-wrapper-button-icon carousel-wrapper-button-icon-next"></span>
    </button>
  </div>
</figure>
```

Empty icon spans draw the default arrows. Button size, radius, colors, the pagination gap and the thumbnail size all come from the `carousel` tokens.

<!-- generated:utilities carousel -->

| Class                               | Styles                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `carousel`                          | `display: flex;`<br>`flex-direction: column;`<br>`row-gap: var(--carousel-gap-y, 16px);`                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `carousel-wrapper`                  | `& { position: relative; overflow: hidden; }`<br>`& .swiper-slide { width: 100%; height: auto; }`<br>`.carousel-wrapper .swiper-slide { > * { &:is(img, video, iframe), img, video, iframe { border-radius: 0; } } }`<br>`figure:not(.figure-w-full):not(.figure-w-screen) .carousel-wrapper { border-radius: var(--carousel-wrapper-border-radius, 0px); border-width: var(--carousel-wrapper-border-width, 0px); border-color: var(--color-carousel-wrapper-border, transparent); }`<br>+ 1 more rule              |
| `carousel-wrapper-pagination`       | `& { z-index: 10; position: absolute; left: 50%; translate: -50% 0; bottom: 24px; display: inline-flex; justify-content: center; align-items: center; column-gap: var(--carousel-wrapper-pagination-gap-x, 8px); }`<br>+ 3 more rules                                                                                                                                                                                                                                                                                |
| `carousel-wrapper-button-prev`      | `& { --tw-shadow-color: var(--color-carousel-wrapper-button-normal-shadow, transparent); cursor: pointer; z-index: 10; position: absolute; top: 50%; translate: 0 -50%; display: inline-grid; place-items: center; width: var(--carousel-wrapper-button-size, 24px); height: var(--carousel-wrapper-button-size, 24px); border-radius: var(--carousel-wrapper-button-border-radius, 9999px); border-width: var(--carousel-wrapper-button-border-width, 0px); box-shadow: var(--carousel-wrapper-…`<br>+ 3 more rules |
| `carousel-wrapper-button-next`      | `& { --tw-shadow-color: var(--color-carousel-wrapper-button-normal-shadow, transparent); cursor: pointer; z-index: 10; position: absolute; top: 50%; translate: 0 -50%; display: inline-grid; place-items: center; width: var(--carousel-wrapper-button-size, 24px); height: var(--carousel-wrapper-button-size, 24px); border-radius: var(--carousel-wrapper-button-border-radius, 9999px); border-width: var(--carousel-wrapper-button-border-width, 0px); box-shadow: var(--carousel-wrapper-…`<br>+ 3 more rules |
| `carousel-wrapper-button-icon-prev` | `div.carousel-wrapper-button-icon-prev:empty { display: inline-block; mask-image: url("data:image/svg+xml,…"); mask-size: var(--carousel-wrapper-button-icon-size, 24px) var(--carousel-wrapper-button-icon-size, 24px); mask-repeat: no-repeat; mask-position: center; background-color: var(--color-carousel-wrapper-button-normal-fill, inherit); transition-property: background-color; transition-duration: inherit; transition-timing-function: inherit; }`<br>+ 3 more rules                                  |
| `carousel-wrapper-button-icon-next` | `div.carousel-wrapper-button-icon-next:empty { display: inline-block; mask-image: url("data:image/svg+xml,…"); mask-size: var(--carousel-wrapper-button-icon-size, 24px) var(--carousel-wrapper-button-icon-size, 24px); mask-repeat: no-repeat; mask-position: center; background-color: var(--color-carousel-wrapper-button-normal-fill, inherit); transition-property: background-color; transition-duration: inherit; transition-timing-function: inherit; }`<br>+ 3 more rules                                  |
| `carousel-thumbs`                   | `& { overflow: hidden; }`<br>`& .swiper-slide { cursor: pointer; overflow: hidden; aspect-ratio: 1/1; transition-property: opacity; width: var(--carousel-thumbs-item-size, 64px); height: var(--carousel-thumbs-item-size, 64px); border-radius: var(--carousel-thumbs-border-radius, 0px); border-width: var(--carousel-thumbs-border-width, 0px); border-color: var(--color-carousel-thumbs-border, currentColor); opacity: calc(var(--color-carousel-thumbs-default-opacity, 50) / 100); }`<br>+ 2 more rules    |
| `carousel-bottom`                   | `display: flex;`<br>`justify-content: space-between;`<br>`align-items: center;`<br>`padding-top: var(--carousel-bottom-padding-top, 0px);`<br>`gap: var(--carousel-bottom-gap-x, 10px);`                                                                                                                                                                                                                                                                                                                             |
| `carousel-bottom-pagination`        | `font-family: var(--font-carousel-bottom-pagination-font-family);`<br>`font-style: var(--font-carousel-bottom-pagination-font-style);`<br>`font-weight: var(--font-carousel-bottom-pagination-font-weight);`<br>`font-size: var(--carousel-bottom-pagination-font-size, 14px);`<br>`line-height: var(--carousel-bottom-pagination-line-height, 100%);`<br>`letter-spacing: var(--carousel-bottom-pagination-letter-spacing, 0px);`<br>`color: var(--color-carousel-bottom-pagination-text, inherit);`                |
| `carousel-bottom-buttons`           | `& { display: flex; align-items: center; justify-content: center; flex-direction: row; gap: var(--carousel-bottom-gap-x, 10px); }`<br>`& .swiper-button-disabled { opacity: 0.5; cursor: not-allowed; }`                                                                                                                                                                                                                                                                                                             |
| `carousel-bottom-button-prev`       | `& { --tw-shadow: var(--tw-shadow-color); --carousel-bottom-button-shadow-x: var(--carousel-bottom-button-shadow-x, 0px); --carousel-bottom-button-shadow-y: var(--carousel-bottom-button-shadow-y, 0px); --carousel-bottom-button-shadow-blur: var(--carousel-bottom-button-shadow-blur, 0px); --carousel-bottom-button-shadow-spread: var(--carousel-bottom-button-shadow-spread, 0px); --tw-shadow-color: var(--color-carousel-bottom-button-normal-shadow, transparent); vertical-align: bot…`<br>+ 1 more rule  |
| `carousel-bottom-button-next`       | `& { --tw-shadow: var(--tw-shadow-color); --carousel-bottom-button-shadow-x: var(--carousel-bottom-button-shadow-x, 0px); --carousel-bottom-button-shadow-y: var(--carousel-bottom-button-shadow-y, 0px); --carousel-bottom-button-shadow-blur: var(--carousel-bottom-button-shadow-blur, 0px); --carousel-bottom-button-shadow-spread: var(--carousel-bottom-button-shadow-spread, 0px); --tw-shadow-color: var(--color-carousel-bottom-button-normal-shadow, transparent); vertical-align: bot…`<br>+ 1 more rule  |
| `carousel-bottom-button-icon`       | `& { display: inline-block; overflow: visible; object-fit: contain; object-position: center; flex-shrink: 0; flex-grow: 0; transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, box-shadow, opacity; width: var(--carousel-bottom-button-icon-size, 16px); height: var(--carousel-bottom-button-icon-size, 16px); color: var(--color-carousel-bottom-button-normal-icon, inherit); }`<br>+ 3 more rules                                                                  |
| `carousel-bottom-button-icon-prev`  | `div.carousel-bottom-button-icon-prev:empty { mask-image: url("data:image/svg+xml,…"); mask-size: var(--carousel-bottom-button-icon-size, 16px) var(--carousel-bottom-button-icon-size, 16px); mask-repeat: no-repeat; mask-position: center; background-color: var(--color-carousel-bottom-button-normal-icon, inherit); transition-property: background-color; transition-duration: inherit; transition-timing-function: inherit; }`<br>+ 3 more rules                                                             |
| `carousel-bottom-button-icon-next`  | `div.carousel-bottom-button-icon-next:empty { mask-image: url("data:image/svg+xml,…"); mask-size: var(--carousel-bottom-button-icon-size, 16px) var(--carousel-bottom-button-icon-size, 16px); mask-repeat: no-repeat; mask-position: center; background-color: var(--color-carousel-bottom-button-normal-icon, inherit); transition-property: background-color; transition-duration: inherit; transition-timing-function: inherit; }`<br>+ 3 more rules                                                             |

<!-- /generated -->

## Next.js

`@systhemaui/next` re-exports `Carousel` and `CarouselItem` from `@systhemaui/react` unchanged. Use the Next `Image` in the slides for optimized files.

## Accessibility

- The carousel answers the keyboard (arrow keys while it is in the viewport), horizontal mouse-wheel and trackpad gestures, dragging and touch.
- Swiper's A11y module adds slide roles and labels and announces slide changes.
- The arrows are icon-only buttons, each with one visually hidden label. Translate them with `labels`.
- Give each slide image a meaningful `alt`, or `alt=""` when the slides are decorative.

## Related

- [Gallery](https://docs.systhema.app/sk/next/components/gallery.md)
- [Figure](https://docs.systhema.app/sk/next/components/figure.md)
- [Carousel block](https://docs.systhema.app/sk/next/payload/blocks/carousel.md)
