Carousel
Native scroll-snap slides with momentum on touch, mouse dragging, arrow keys, dots, thumbnails and an autoplay that pauses when it should.
pnpm dlx shadcn@latest add https://hextaui.com/r/carousel.jsonAdds the component, the HextaUI theme tokens and any HextaUI components it depends on.
Add the theme tokens to your global CSS, if you haven’t yet.
Install the dependencies.
pnpm add @base-ui/react @tabler/icons-react class-variance-authority cnCopy and paste the following code into your project.
components/ui/carousel.tsx components/ui/button.tsx components/ui/number-flow.tsx lib/motion.ts Update the import paths to match your project setup.
Slides scroll natively with CSS scroll snap, so touch momentum and trackpad scrolling feel like the platform. A mouse can drag, and the arrow keys move one slide at a time.
Pass setApi to get the carousel API, then listen for select to show your own position. Next is disabled on the last slide but keeps focus.
Items set their own basis. Gutters come from the spacing prop, so they stay exact at any basis.
The active dot stretches as the slides scroll and its neighbours make room. The counter only spins the digit that changed.
autoplay is off by default and stays off with reduced motion. It pauses on hover, keyboard focus, touch, drag, a hidden tab, or when scrolled out of view, and the active dot fills as the timer runs.
<CarouselThumbnails /> follows the main carousel and scrolls to keep the active thumbnail in view.
orientation="vertical" needs a height on <CarouselContent />. The buttons move above and below.
Pass index and onIndexChange. Swiping updates your state and your state scrolls the carousel.
rewind sends Next on the last slide back to the first. defaultIndex opens on a slide without a scroll animation.
Dragging a link with the mouse scrolls without opening it. Tabbing to a slide that is off screen scrolls it into view.
Dots, the counter and the buttons update as slides come and go.
Arrow keys, dragging and dots only move the carousel you are in.
Unbroken text wraps inside its slide. With one slide the dots hide and the buttons stay disabled.
Slides start on the right, the arrows flip, the Left arrow key moves forward and the dots fill from the right.
Keys work while focus is anywhere inside the carousel, except in text fields and nested carousels.
| Key | Action |
|---|---|
| → | Next slide. Previous in right-to-left layouts. ↓ in vertical carousels. |
| ← | Previous slide. Next in right-to-left layouts. ↑ in vertical carousels. |
| Tab | Moves through the buttons, the active dot and the slides’ content, scrolling off-screen slides into view. |
| EnterSpace | Activates the focused button, dot or thumbnail. |
- The root is a
regiondescribed as a carousel. Give it anaria-label. - Each item is a
groupdescribed as a slide and labelled with its position, like “3 of 5”. - A polite live region announces the new slide after keyboard or button navigation, and stays quiet while autoplay runs.
- Dots and thumbnails use a single tab stop, and focus follows the active one.
- Previous and Next stay focusable when disabled, so focus is never lost at either end.
| Prop | Type | Default |
|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" |
spacingGap between slides. | "none" | "sm" | "default" | "lg" | "default" |
index | number | – |
defaultIndex | number | 0 |
onIndexChange | (index: number) => void | – |
rewindWrap from the last slide back to the first. | boolean | false |
mouseDragLet a mouse drag the slides. | boolean | true |
autoplayAdvance on a timer. delay defaults to 5000ms, minimum 1000ms. | boolean | { delay?: number } | false |
setApi | (api: CarouselApi) => void | – |
render | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="carousel" | Target the root in CSS. |
data-orientation | The orientation. |
--carousel-spacing | The gap between slides, set by spacing. |
| Prop | Type | Default |
|---|---|---|
classNameApplied to the track that holds the slides. | string | – |
viewportClassNameApplied to the scrolling viewport. | string | – |
| Attribute | Description |
|---|---|
data-slot="carousel-content" | The scrolling viewport. |
data-slot="carousel-container" | The track inside it. |
data-scrollable | Present when there is more than one position. |
data-dragging | Present while a mouse drag is in progress. |
| Prop | Type | Default |
|---|---|---|
render | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="carousel-item" | Set basis-* to show several per view. |
Both render a <Button /> and accept its props. They sit outside the content, so leave room around the carousel.
| Prop | Type | Default |
|---|---|---|
variant | ButtonProps["variant"] | "outline" |
size | ButtonProps["size"] | "icon-sm" |
childrenFollows the orientation and direction. | ReactNode | Arrow icon |
| Attribute | Description |
|---|---|
data-slot="carousel-previous" | Labelled “Previous slide”. |
data-slot="carousel-next" | Labelled “Next slide”. |
data-disabled | Present at either end. The button stays focusable. |
| Prop | Type | Default |
|---|---|---|
aria-label | string | "Choose slide" |
| Attribute | Description |
|---|---|
data-slot="carousel-dots" | The group of dots. Hidden with one position. |
data-slot="carousel-dot" | Each dot. The active one has aria-current. |
--dot-active | 0 to 1, how active a dot is while scrolling. |
| Attribute | Description |
|---|---|
data-slot="carousel-counter" | Shows the current position over the total with a rolling digit. |
| Prop | Type | Default |
|---|---|---|
variant | ButtonProps["variant"] | "ghost" |
size | ButtonProps["size"] | "icon-sm" |
| Attribute | Description |
|---|---|
data-slot="carousel-autoplay-toggle" | Labelled “Pause slideshow” or “Play slideshow”. |
| Prop | Type | Default |
|---|---|---|
aria-label | string | "Slides" |
| Attribute | Description |
|---|---|
data-slot="carousel-thumbnails" | The scrolling strip. |
| Prop | Type | Default |
|---|---|---|
indexThe slide it opens. Defaults to its position in the strip. | number | – |
render | ReactElement | (props, state) => ReactElement | <button> |
| Attribute | Description |
|---|---|
data-slot="carousel-thumbnail" | Target thumbnails in CSS. |
data-active | Present while its slide is in view. |
Returned through setApi and useCarousel(). Pass jump: true to move without animating.
| Prop | Type | Default |
|---|---|---|
scrollPrev | (jump?: boolean) => void | – |
scrollNext | (jump?: boolean) => void | – |
scrollToScrolls to a snap position. | (index: number, jump?: boolean) => void | – |
scrollToSlideScrolls to the position that shows a slide. | (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 | – |
Use inside <Carousel /> to build your own controls. Returns api, orientation, selectedIndex, snapCount, slideCount, slidesInView, canScrollPrev, canScrollNext, isPlaying and the scroll and play methods.
- ButtonButtons in every variant and size, with a built-in loading, success and error flow that skips the spinner for fast requests.
- Number flowAnimated numbers where only the changed digits spin, with any Intl format and locale.
- AccordionStacked headings that each reveal a panel, with height motion you can reverse mid-way and panels that stay searchable while closed.
- Aspect ratioA box that holds its shape before media loads, shimmers while loading, fades the media in and falls back when it fails.
- CollapsibleA panel that shows and hides with height motion you can reverse mid-way, without jumping the layout.
- ResizablePanels you can drag apart, with a quiet divider that wakes up on hover, sizes that glide on reset or collapse, and layouts that persist.