HextaUI

Carousel

Native Scroll-Snap-Slides mit Momentum auf Touch, Maus-Ziehen, Pfeiltasten, Punkten, Thumbnails und einem Autoplay, das pausiert, wenn es soll.

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

const slides = [1, 2, 3, 4, 5]

export function CarouselDemo() {
  return (
    <div className="w-full max-w-xs px-12">
      <Carousel aria-label="Numbers">
        <CarouselContent>
          {slides.map((value) => (
            <CarouselItem key={value}>
              <div className="flex aspect-square items-center justify-center rounded-xl border bg-card p-6 text-4xl font-semibold">
                {value}
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>
        <CarouselPrevious />
        <CarouselNext />
        <div className="mt-3 flex justify-center">
          <CarouselDots />
        </div>
      </Carousel>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/carousel.json

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

import {
  Carousel,
  CarouselContent,
  CarouselDots,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel"
<Carousel aria-label="Featured">
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
  <CarouselDots />
</Carousel>

Folien scrollen nativ mit CSS Scroll Snap, sodass Touch-Schwung und Trackpad-Scrollen sich wie die Plattform anfühlen. Eine Maus kann ziehen, und die Pfeiltasten bewegen jeweils eine Folie.

Carousel
├── CarouselContent
│   └── CarouselItem
├── CarouselPrevious
├── CarouselNext
├── CarouselDots
├── CarouselCounter
├── CarouselAutoplayToggle
└── CarouselThumbnails
    └── CarouselThumbnail

API

Übergib setApi, um die Carousel-API zu erhalten, und höre auf select, um deine eigene Position anzuzeigen. Next ist auf der letzten Folie deaktiviert, behält aber den Fokus.

"use client"

import * as React from "react"

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

const slides = [1, 2, 3, 4, 5]

export function CarouselWithApi() {
  const [api, setApi] = React.useState<CarouselApi>()
  const [current, setCurrent] = React.useState(0)
  const [count, setCount] = React.useState(0)

  React.useEffect(() => {
    if (!api) {
      return
    }
    const sync = () => {
      setCount(api.scrollSnapList().length)
      setCurrent(api.selectedScrollSnap() + 1)
    }
    sync()
    api.on("select", sync).on("reInit", sync)
    return () => {
      api.off("select", sync).off("reInit", sync)
    }
  }, [api])

  return (
    <div className="flex w-full max-w-xs flex-col items-center gap-2 px-12">
      <Carousel setApi={setApi} aria-label="Numbers" className="w-full">
        <CarouselContent>
          {slides.map((value) => (
            <CarouselItem key={value}>
              <div className="flex aspect-square items-center justify-center rounded-xl border bg-card p-6 text-4xl font-semibold">
                {value}
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>
        <CarouselPrevious />
        <CarouselNext />
      </Carousel>
      <p className="text-sm text-muted-foreground">
        Slide {current} of {count}
      </p>
    </div>
  )
}

Mehrere pro Ansicht

Elemente setzen ihre eigene basis. Die Abstände kommen aus der spacing-Prop und bleiben deshalb bei jeder Basis exakt.

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

export function CarouselSeveralPerView() {
  return (
    <div className="w-full max-w-md px-12">
      <Carousel spacing="sm" aria-label="Several per view">
        <CarouselContent>
          {Array.from({ length: 9 }, (_, index) => (
            <CarouselItem key={index} className="basis-1/2 sm:basis-1/3">
              <div className="flex aspect-square items-center justify-center rounded-xl border bg-card p-6 text-3xl font-semibold">
                {index + 1}
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>
        <CarouselPrevious />
        <CarouselNext />
      </Carousel>
    </div>
  )
}

Punkte und Zähler

Der aktive Punkt dehnt sich, während die Folien scrollen, und seine Nachbarn machen Platz. Der Zähler dreht nur die Ziffer, die sich geändert hat.

import {
  Carousel,
  CarouselContent,
  CarouselCounter,
  CarouselDots,
  CarouselItem,
} from "@/components/ui/carousel"

export function CarouselDotsAndCounter() {
  return (
    <Carousel aria-label="Dots and counter" className="w-full max-w-md">
      <CarouselContent>
        {Array.from({ length: 12 }, (_, index) => (
          <CarouselItem key={index} className="basis-4/5">
            <div className="flex aspect-square items-center justify-center rounded-xl border bg-card p-6 text-4xl font-semibold">
              {index + 1}
            </div>
          </CarouselItem>
        ))}
      </CarouselContent>
      <div className="mt-3 flex items-center justify-between gap-4">
        <CarouselCounter />
        <CarouselDots />
      </div>
    </Carousel>
  )
}

Autoplay

autoplay ist standardmäßig aus und bleibt bei reduzierter Bewegung aus. Es pausiert bei Hover, Tastaturfokus, Touch, Drag, einem verborgenen Tab oder wenn es aus dem Sichtbereich gescrollt wird, und der aktive Punkt füllt sich, während der Timer läuft.

import {
  Carousel,
  CarouselAutoplayToggle,
  CarouselContent,
  CarouselDots,
  CarouselItem,
} from "@/components/ui/carousel"

const slides = [1, 2, 3, 4, 5]

export function CarouselAutoplay() {
  return (
    <Carousel
      autoplay={{ delay: 3000 }}
      aria-label="Autoplay"
      className="w-full max-w-xs"
    >
      <CarouselContent>
        {slides.map((value) => (
          <CarouselItem key={value}>
            <div className="flex aspect-square items-center justify-center rounded-xl border bg-card p-6 text-4xl font-semibold">
              {value}
            </div>
          </CarouselItem>
        ))}
      </CarouselContent>
      <div className="mt-3 flex items-center justify-center gap-2">
        <CarouselAutoplayToggle />
        <CarouselDots />
      </div>
    </Carousel>
  )
}

Thumbnails

<CarouselThumbnails /> folgt dem Haupt-Carousel und scrollt, um das aktive Thumbnail im Blick zu halten.

import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselThumbnail,
  CarouselThumbnails,
} from "@/components/ui/carousel"

const photos = Array.from({ length: 10 }, (_, index) => index + 1)

export function CarouselWithThumbnails() {
  return (
    <Carousel aria-label="Gallery" className="w-full max-w-md">
      <CarouselContent>
        {photos.map((photo) => (
          <CarouselItem key={photo}>
            <div className="relative overflow-hidden rounded-xl border">
              <img
                src="/preview/landscape.svg"
                alt={`Landscape ${photo}`}
                draggable={false}
                className="aspect-video w-full object-cover"
              />
              <span className="absolute start-3 top-3 rounded-md bg-background px-2 py-1 text-xs font-medium">
                {photo}
              </span>
            </div>
          </CarouselItem>
        ))}
      </CarouselContent>
      <div className="mt-3">
        <CarouselThumbnails>
          {photos.map((photo) => (
            <CarouselThumbnail key={photo} className="w-20">
              <img
                src="/preview/landscape.svg"
                alt=""
                draggable={false}
                className="aspect-video w-full object-cover"
              />
            </CarouselThumbnail>
          ))}
        </CarouselThumbnails>
      </div>
    </Carousel>
  )
}

Vertikal

orientation="vertical" braucht eine Höhe auf <CarouselContent />. Die Buttons wandern nach oben und unten.

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

const slides = [1, 2, 3, 4, 5]

export function CarouselVertical() {
  return (
    <div className="w-full max-w-xs py-12">
      <Carousel orientation="vertical" aria-label="Vertical">
        <CarouselContent className="h-52">
          {slides.map((value) => (
            <CarouselItem key={value} className="basis-1/2">
              <div className="flex h-full items-center justify-center rounded-xl border bg-card p-6 text-3xl font-semibold">
                {value}
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>
        <CarouselPrevious />
        <CarouselNext />
      </Carousel>
    </div>
  )
}

Kontrolliert

Übergib index und onIndexChange. Wischen aktualisiert deinen State, und dein State scrollt das Carousel.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Carousel,
  CarouselContent,
  CarouselDots,
  CarouselItem,
} from "@/components/ui/carousel"

const slides = [1, 2, 3, 4, 5]

export function CarouselControlled() {
  const [index, setIndex] = React.useState(2)

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <div className="flex flex-wrap items-center gap-2">
        {slides.map((value, position) => (
          <Button
            key={value}
            size="sm"
            variant={position === index ? "secondary" : "outline"}
            onClick={() => setIndex(position)}
          >
            {value}
          </Button>
        ))}
        <span className="text-sm text-muted-foreground">index = {index}</span>
      </div>
      <Carousel index={index} onIndexChange={setIndex} aria-label="Controlled">
        <CarouselContent>
          {slides.map((value) => (
            <CarouselItem key={value}>
              <div className="flex aspect-square items-center justify-center rounded-xl border bg-card p-6 text-4xl font-semibold">
                {value}
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>
        <div className="mt-3 flex justify-center">
          <CarouselDots />
        </div>
      </Carousel>
    </div>
  )
}

Zurückspulen und Startindex

rewind schickt Next auf der letzten Folie zurück zur ersten. defaultIndex öffnet auf einer Folie ohne Scroll-Animation.

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

const slides = [1, 2, 3, 4, 5]

export function CarouselRewind() {
  return (
    <div className="w-full max-w-xs px-12">
      <Carousel rewind defaultIndex={2} aria-label="Rewind">
        <CarouselContent>
          {slides.map((value) => (
            <CarouselItem key={value}>
              <div className="flex aspect-square items-center justify-center rounded-xl border bg-card p-6 text-4xl font-semibold">
                {value}
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>
        <CarouselPrevious />
        <CarouselNext />
      </Carousel>
    </div>
  )
}

Ein Link, den du mit der Maus ziehst, scrollt, ohne geöffnet zu werden. Das Tabben zu einer Folie außerhalb des Bildschirms scrollt sie in den Sichtbereich.

import {
  Carousel,
  CarouselContent,
  CarouselDots,
  CarouselItem,
} from "@/components/ui/carousel"

const articles = Array.from({ length: 6 }, (_, index) => index + 1)

export function CarouselLinks() {
  return (
    <Carousel spacing="sm" aria-label="Articles" className="w-full max-w-md">
      <CarouselContent>
        {articles.map((article) => (
          <CarouselItem key={article} className="basis-4/5 sm:basis-1/2">
            <a
              href="#"
              className="flex h-32 flex-col justify-end gap-1 rounded-xl border bg-card p-4 outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden"
            >
              <span className="text-sm font-medium">Article {article}</span>
              <span className="text-sm text-muted-foreground">
                Read the full story
              </span>
            </a>
          </CarouselItem>
        ))}
      </CarouselContent>
      <div className="mt-3 flex justify-center">
        <CarouselDots />
      </div>
    </Carousel>
  )
}

Folien hinzufügen und entfernen

Punkte, Zähler und Buttons aktualisieren sich, wenn Folien hinzukommen und wegfallen.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Carousel,
  CarouselContent,
  CarouselCounter,
  CarouselDots,
  CarouselItem,
} from "@/components/ui/carousel"

export function CarouselDynamic() {
  const [slides, setSlides] = React.useState([1, 2, 3])

  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <div className="flex gap-2">
        <Button
          size="sm"
          variant="outline"
          onClick={() => setSlides([...slides, slides.length + 1])}
        >
          Add slide
        </Button>
        <Button
          size="sm"
          variant="outline"
          disabled={slides.length === 0}
          onClick={() => setSlides(slides.slice(0, -1))}
        >
          Remove slide
        </Button>
      </div>
      <Carousel aria-label="Dynamic">
        <CarouselContent>
          {slides.map((value) => (
            <CarouselItem key={value} className="basis-1/2">
              <div className="flex aspect-square items-center justify-center rounded-xl border bg-card p-6 text-4xl font-semibold">
                {value}
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>
        <div className="mt-3 flex items-center justify-between">
          <CarouselCounter />
          <CarouselDots />
        </div>
      </Carousel>
    </div>
  )
}

Verschachtelt

Pfeiltasten, Ziehen und Punkte bewegen nur das Carousel, in dem du dich befindest.

import {
  Carousel,
  CarouselContent,
  CarouselDots,
  CarouselItem,
} from "@/components/ui/carousel"

const outerSlides = [1, 2, 3]
const innerSlides = [1, 2, 3, 4, 5]

export function CarouselNested() {
  return (
    <Carousel aria-label="Outer" className="w-full max-w-md">
      <CarouselContent>
        {outerSlides.map((outer) => (
          <CarouselItem key={outer}>
            <div className="flex flex-col gap-3 rounded-xl border bg-card p-4">
              <p className="text-sm font-medium">Outer slide {outer}</p>
              <Carousel spacing="sm" aria-label={`Inner ${outer}`}>
                <CarouselContent>
                  {innerSlides.map((inner) => (
                    <CarouselItem key={inner} className="basis-1/3">
                      <div className="flex aspect-square items-center justify-center rounded-lg bg-muted text-lg font-medium">
                        {outer}.{inner}
                      </div>
                    </CarouselItem>
                  ))}
                </CarouselContent>
                <div className="mt-2 flex justify-center">
                  <CarouselDots />
                </div>
              </Carousel>
            </div>
          </CarouselItem>
        ))}
      </CarouselContent>
      <div className="mt-3 flex justify-center">
        <CarouselDots />
      </div>
    </Carousel>
  )
}

Langer Inhalt und eine einzelne Folie

Text ohne Umbruchstelle wird in der Folie umgebrochen. Bei nur einer Folie werden die Punkte ausgeblendet, und die Buttons bleiben deaktiviert.

import {
  Carousel,
  CarouselContent,
  CarouselCounter,
  CarouselDots,
  CarouselItem,
} from "@/components/ui/carousel"

export function CarouselLongContent() {
  return (
    <div className="flex w-full max-w-md flex-col gap-6">
      <Carousel aria-label="Long content">
        <CarouselContent>
          <CarouselItem className="basis-4/5">
            <div className="rounded-xl border bg-card p-4 text-sm wrap-anywhere">
              Supercalifragilisticexpialidocious_with_an_unbroken_string_that_never_ends_and_keeps_going_well_past_the_edge
            </div>
          </CarouselItem>
          <CarouselItem className="basis-4/5">
            <div className="rounded-xl border bg-card p-4 text-sm">
              مرحبا 你好 👩‍👩‍👧‍👦 A second slide with mixed scripts.
            </div>
          </CarouselItem>
        </CarouselContent>
        <div className="mt-3 flex justify-center">
          <CarouselDots />
        </div>
      </Carousel>
      <Carousel aria-label="Single slide">
        <CarouselContent>
          <CarouselItem>
            <div className="flex aspect-video items-center justify-center rounded-xl border bg-card p-6 text-2xl font-semibold">
              Only one
            </div>
          </CarouselItem>
        </CarouselContent>
        <div className="mt-3 flex items-center justify-between">
          <CarouselCounter />
          <CarouselDots />
        </div>
      </Carousel>
    </div>
  )
}

Rechts nach links

Folien beginnen rechts, die Pfeile kehren sich um, die Taste Pfeil links geht vorwärts, und die Punkte füllen sich von rechts.

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

const slides = [1, 2, 3, 4, 5]

export function CarouselRtl() {
  return (
    <div dir="rtl" className="w-full max-w-md px-12">
      <Carousel autoplay={{ delay: 4000 }} aria-label="شرائح">
        <CarouselContent>
          {slides.map((value) => (
            <CarouselItem key={value} className="basis-1/2">
              <div className="flex aspect-square items-center justify-center rounded-xl border bg-card p-6 text-4xl font-semibold">
                {value}
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>
        <CarouselPrevious />
        <CarouselNext />
        <div className="mt-3 flex items-center justify-between">
          <CarouselCounter />
          <CarouselDots />
        </div>
      </Carousel>
    </div>
  )
}

Die Tasten funktionieren, solange der Fokus irgendwo im Carousel liegt, außer in Textfeldern und verschachtelten Carousels.

TasteAktion
→Nächste Folie. Vorherige in Rechts-nach-links-Layouts. ↓ in vertikalen Carousels.
←Vorherige Folie. Nächste in Rechts-nach-links-Layouts. ↑ in vertikalen Carousels.
TabGeht durch die Buttons, den aktiven Punkt und den Inhalt der Folien und scrollt Folien außerhalb des Bildschirms in den Sichtbereich.
EnterSpaceAktiviert den fokussierten Button, Punkt oder das fokussierte Thumbnail.
  • Die Root ist eine region, die als Carousel beschrieben ist. Gib ihr ein aria-label.
  • Jedes Element ist eine group, die als Folie beschrieben und mit ihrer Position beschriftet ist, etwa „3 of 5“.
  • Eine Live-Region mit polite-Priorität sagt die neue Folie nach der Navigation per Tastatur oder Button an und bleibt still, solange Autoplay läuft.
  • Punkte und Thumbnails nutzen einen einzigen Tab-Stopp, und der Fokus folgt dem aktiven.
  • Previous und Next bleiben im deaktivierten Zustand fokussierbar, sodass der Fokus an keinem Ende verloren geht.
PropTypStandard
orientation
"horizontal" | "vertical""horizontal"
spacingAbstand zwischen den Folien.
"none" | "sm" | "default" | "lg""default"
index
number–
defaultIndex
number0
onIndexChange
(index: number) => void–
rewindVon der letzten Folie zurück zur ersten springen.
booleanfalse
mouseDragDie Folien mit der Maus ziehen lassen.
booleantrue
autoplayPer Timer weiterschalten. delay beträgt standardmäßig 5000 ms, mindestens 1000 ms.
boolean | { delay?: number }false
setApi
(api: CarouselApi) => void–
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="carousel"Das Root in CSS ansprechen.
data-orientationDie Ausrichtung.
--carousel-spacingDer Abstand zwischen den Folien, festgelegt durch spacing.
PropTypStandard
classNameWird auf die Spur angewendet, die die Folien enthält.
string–
viewportClassNameWird auf den scrollenden Viewport angewendet.
string–
AttributBeschreibung
data-slot="carousel-content"Der scrollende Viewport.
data-slot="carousel-container"Die Spur darin.
data-scrollableVorhanden, wenn es mehr als eine Position gibt.
data-draggingVorhanden, solange ein Maus-Drag läuft.
PropTypStandard
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="carousel-item"Setze basis-*, um mehrere pro Ansicht zu zeigen.

Beide rendern ein <Button /> und akzeptieren dessen Props. Sie sitzen außerhalb des Inhalts, lass also Platz um das Carousel.

PropTypStandard
variant
ButtonProps["variant"]"outline"
size
ButtonProps["size"]"icon-sm"
childrenFolgt der Ausrichtung und der Richtung.
ReactNodeArrow icon
AttributBeschreibung
data-slot="carousel-previous"Beschriftet mit „Previous slide“.
data-slot="carousel-next"Beschriftet mit „Next slide“.
data-disabledAn beiden Enden vorhanden. Der Button bleibt fokussierbar.
PropTypStandard
aria-label
string"Choose slide"
AttributBeschreibung
data-slot="carousel-dots"Die Gruppe der Punkte. Bei nur einer Position ausgeblendet.
data-slot="carousel-dot"Jeder Punkt. Der aktive hat aria-current.
--dot-active0 bis 1, wie aktiv ein Punkt beim Scrollen ist.
AttributBeschreibung
data-slot="carousel-counter"Zeigt die aktuelle Position über der Gesamtzahl mit einer rollenden Ziffer.
PropTypStandard
variant
ButtonProps["variant"]"ghost"
size
ButtonProps["size"]"icon-sm"
AttributBeschreibung
data-slot="carousel-autoplay-toggle"Beschriftet mit „Pause slideshow“ oder „Play slideshow“.
PropTypStandard
aria-label
string"Slides"
AttributBeschreibung
data-slot="carousel-thumbnails"Der scrollende Streifen.
PropTypStandard
indexDie Folie, die sie öffnet. Standardmäßig ihre Position im Streifen.
number–
render
ReactElement | (props, state) => ReactElement<button>
AttributBeschreibung
data-slot="carousel-thumbnail"Thumbnails in CSS ansprechen.
data-activeVorhanden, solange die Folie im Sichtbereich ist.

Wird über setApi und useCarousel() zurückgegeben. Übergib jump: true, um sich ohne Animation zu bewegen.

PropTypStandard
scrollPrev
(jump?: boolean) => void–
scrollNext
(jump?: boolean) => void–
scrollToScrollt zu einer Snap-Position.
(index: number, jump?: boolean) => void–
scrollToSlideScrollt zu der Position, die eine Folie zeigt.
(slideIndex: number, jump?: boolean) => void–
canScrollPrev
() => boolean–
canScrollNext
() => boolean–
selectedScrollSnap
() => number–
scrollSnapList
() => number[]–
slidesInView
() => number[]–
slideNodes
() => HTMLElement[]–
viewportNode
() => HTMLElement | null–
play
() => void–
stop
() => void–
isPlaying
() => boolean–
on / off
(event: "select" | "scroll" | "settle" | "reInit", listener) => CarouselApi–

Verwende es innerhalb von <Carousel />, um eigene Bedienelemente zu bauen. Gibt api, orientation, selectedIndex, snapCount, slideCount, slidesInView, canScrollPrev, canScrollNext, isPlaying sowie die Scroll- und Wiedergabemethoden zurück.