Shadcn Carousel for React and Tailwind
An interactive slideshow for cycling through items.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/carousel.jsonpnpm dlx shadcn@latest add https://kit.dev/r/carousel.jsonnpx shadcn@latest add https://kit.dev/r/carousel.jsonyarn shadcn@latest add https://kit.dev/r/carousel.json<Step>This component depends on Button. Install it first if you haven't already.</Step>
Install the following dependencies:
bun add @ark-ui/react lucide-reactpnpm add @ark-ui/react lucide-reactnpm install @ark-ui/react lucide-reactyarn add @ark-ui/react lucide-reactCopy and paste the following code into your project.
"use client";
import {
Carousel as ArkCarousel,
useCarousel as useArkCarousel,
useCarouselContext as useArkCarouselContext,
} from "@ark-ui/react/carousel";
import {
ChevronLeftIcon,
ChevronRightIcon,
PauseIcon,
PlayIcon,
} from "lucide-react";
import type React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
export const useCarousel: typeof useArkCarousel = (props) =>
useArkCarousel({ spacing: "16px", ...props });
export const useCarouselContext = useArkCarouselContext;
export const CarouselContext = ArkCarousel.Context;
const carouselRootClassName = cn(
"relative",
"flex flex-col",
"data-[orientation=vertical]:w-max data-[orientation=vertical]:flex-row"
);
export const Carousel = (
props: React.ComponentProps<typeof ArkCarousel.Root>
) => {
const { spacing = "16px", className, ...rest } = props;
return (
<ArkCarousel.Root
className={cn(carouselRootClassName, className)}
data-slot="carousel"
spacing={spacing}
{...rest}
/>
);
};
export const CarouselRootProvider = (
props: React.ComponentProps<typeof ArkCarousel.RootProvider>
) => {
const { className, ...rest } = props;
return (
<ArkCarousel.RootProvider
className={cn(carouselRootClassName, className)}
data-slot="carousel"
{...rest}
/>
);
};
export const CarouselControl = (
props: React.ComponentProps<typeof ArkCarousel.Control>
) => {
const { className, ...rest } = props;
return (
<ArkCarousel.Control
className={cn(
"flex items-center justify-between gap-2",
"data-[orientation=vertical]:flex-col",
className
)}
data-slot="carousel-control"
{...rest}
/>
);
};
export const CarouselPrevious = (
props: React.ComponentProps<typeof ArkCarousel.PrevTrigger>
) => {
const { className, ...rest } = props;
return (
<ArkCarousel.PrevTrigger
className={cn(
"absolute",
"data-[orientation=horizontal]:-inset-s-12 data-[orientation=horizontal]:top-1/2 data-[orientation=horizontal]:-translate-y-1/2",
"data-[orientation=vertical]:-top-12 data-[orientation=vertical]:left-1/2 data-[orientation=vertical]:-translate-x-1/2 data-[orientation=vertical]:rotate-90",
className
)}
data-slot="carousel-previous"
{...rest}
asChild
>
<Button
aria-label="Previous slide"
clickEffect={false}
pill
size="icon-md"
variant="outline"
>
<ChevronLeftIcon aria-hidden="true" />
</Button>
</ArkCarousel.PrevTrigger>
);
};
export const CarouselNext = (
props: React.ComponentProps<typeof ArkCarousel.NextTrigger>
) => {
const { className, ...rest } = props;
return (
<ArkCarousel.NextTrigger
className={cn(
"absolute",
"data-[orientation=horizontal]:-inset-e-12 data-[orientation=horizontal]:top-1/2 data-[orientation=horizontal]:-translate-y-1/2",
"data-[orientation=vertical]:-bottom-12 data-[orientation=vertical]:left-1/2 data-[orientation=vertical]:-translate-x-1/2 data-[orientation=vertical]:rotate-90",
className
)}
{...rest}
asChild
data-slot="carousel-next"
>
<Button
aria-label="Next slide"
clickEffect={false}
pill
size="icon-md"
variant="outline"
>
<ChevronRightIcon aria-hidden="true" />
</Button>
</ArkCarousel.NextTrigger>
);
};
export const CarouselIndicatorGroup = (
props: React.ComponentProps<typeof ArkCarousel.IndicatorGroup>
) => {
const { className, ...rest } = props;
return (
<ArkCarousel.IndicatorGroup
className={cn(
"flex justify-center gap-2",
"data-[orientation=vertical]:flex-col",
className
)}
data-slot="carousel-indicator-group"
{...rest}
/>
);
};
export const CarouselIndicator = (
props: React.ComponentProps<typeof ArkCarousel.Indicator>
) => {
const { className, ...rest } = props;
return (
<ArkCarousel.Indicator
className={cn(
"size-2",
"shrink-0",
"bg-foreground",
"opacity-64 data-current:opacity-100",
"overflow-hidden",
"[&_img]:size-full [&_img]:rounded-lg [&_img]:object-cover",
"rounded-full",
className
)}
data-slot="carousel-indicator"
{...rest}
/>
);
};
export const CarouselContent = (
props: React.ComponentProps<typeof ArkCarousel.ItemGroup>
) => {
const { className, ...rest } = props;
return (
<ArkCarousel.ItemGroup
className={cn(
"min-w-0",
"-my-4 py-4",
"overflow-hidden rounded-lg",
className
)}
data-slot="carousel-group"
{...rest}
/>
);
};
export const CarouselItem = (
props: React.ComponentProps<typeof ArkCarousel.Item>
) => {
const { className, ...rest } = props;
return (
<ArkCarousel.Item
className={cn(
"min-w-0",
"[&_img]:size-full [&_img]:rounded-lg [&_img]:object-cover",
className
)}
data-slot="carousel-item"
{...rest}
/>
);
};
export const CarouselAutoplayTrigger = (
props: React.ComponentProps<typeof ArkCarousel.AutoplayTrigger>
) => {
const { className, children, ...rest } = props;
return (
<ArkCarousel.AutoplayTrigger
className={className}
data-slot="carousel-autoplay-trigger"
{...rest}
asChild
>
{children ?? (
<Button
aria-label="Toggle autoplay"
clickEffect={false}
pill
size="icon-md"
variant="outline"
>
<ArkCarousel.AutoplayIndicator
fallback={<PlayIcon aria-hidden="true" />}
>
<PauseIcon aria-hidden="true" />
</ArkCarousel.AutoplayIndicator>
</Button>
)}
</ArkCarousel.AutoplayTrigger>
);
};
export const CarouselAutoplayIndicator = (
props: React.ComponentProps<typeof ArkCarousel.AutoplayIndicator>
) => (
<ArkCarousel.AutoplayIndicator
data-slot="carousel-autoplay-indicator"
{...props}
/>
);
export const CarouselProgressText = (
props: React.ComponentProps<typeof ArkCarousel.ProgressText>
) => {
const { className, ...rest } = props;
return (
<ArkCarousel.ProgressText
className={cn("text-muted-foreground text-sm", className)}
data-slot="carousel-progress-text"
{...rest}
/>
);
};Update the import paths to match your project setup.
Anatomy
Carousel
├── CarouselControl
│ ├── CarouselPrevious
│ ├── CarouselAutoplayTrigger
│ │ └── CarouselAutoplayIndicator
│ └── CarouselNext
├── CarouselContent
│ └── CarouselItem
├── CarouselIndicatorGroup
│ └── CarouselIndicator
└── CarouselProgressTextCarousel maps to Ark Carousel.Root. CarouselContent is the item group (track). CarouselPrevious / CarouselNext are the prev/next triggers. slideCount is required so snap points can be computed (including SSR).
Usage
import {
Carousel,
CarouselContent,
CarouselControl,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";<Carousel slideCount={slides.length}>
<CarouselControl>
<CarouselPrevious />
<CarouselNext />
</CarouselControl>
<CarouselContent>
{slides.map((slide, index) => (
<CarouselItem index={index} key={slide}>
{slide}
</CarouselItem>
))}
</CarouselContent>
</Carousel>shadcn.io defaults spacing to 16px (Ark: 0px). Prev/next always render as icon Buttons; extra children on those triggers are not used. Accessible names default to "Previous slide" / "Next slide".
The track is a CSS grid. Size and gap come from --slide-item-size and --slide-spacing on the root, not from Tailwind flex / gap-* on CarouselContent.
Controlled
Use page and onPageChange. page is the snap page, not necessarily the item index.
Root Provider
Use useCarousel with CarouselRootProvider when you need the API outside the tree. Pass slideCount (and spacing if you want the shadcn.io default) to useCarousel(), not to the provider.
Orientation
Horizontal
Vertical
orientation="vertical" scrolls the track on the block axis. Prev/next rotate to match.
Thumbnail indicators
Render a preview inside each CarouselIndicator. Thumbnail images can use alt="" when the indicator has an accessible name (aria-label or the default "Go to slide N").
Horizontal
Vertical
Examples
Autoplay
autoplay (default delay 4000ms) with loop. CarouselAutoplayTrigger toggles play/pause. Zag also turns loop on when autoplay is set unless you pass loop yourself.
Pause on hover
Call pause() / play() from CarouselContext on pointer enter/leave. This is not a built-in prop.
Loop
Slides per page
slidesPerPage shows more than one slide. Render indicators from pageSnapPoints, not from every item.
Spacing
shadcn.io default is 16px. Combine with slidesPerPage (including fractional values such as 1.5) to peek at adjacent slides.
Variable sizes
autoSize lets each item set its own width. Use snapAlign per item. Give the item a width (className="w-auto" plus a sized child).
Mouse drag
Scroll to slide
scrollToIndex(index) jumps to an item. scrollTo(page) jumps to a page.
Dynamic slides
Keep slideCount and page in sync when the list grows or shrinks. The machine refreshes snap points when slideCount changes.
Guides
Pages vs slides
page is the snap page, not the item index. With slidesPerPage={2}, two items share a page. Indicators should map carousel.pageSnapPoints.
CSS variables
Set on the root by the machine. The item group uses them for grid sizing and gap:
| CSS variable | Description |
|---|---|
--slides-per-page | Visible slides per page |
--slide-spacing | Gap between slides (spacing prop) |
--slide-item-size | calc(100% / var(--slides-per-page) - var(--slide-spacing) * (var(--slides-per-page) - 1) / var(--slides-per-page)), or auto when autoSize |
API Reference
shadcn.io wraps Ark UI Carousel. Defaults below are shadcn.io values. spacing defaults to 16px (Ark: 0px).
asChild merges props onto a single child element.
Carousel
Root. Renders a div with role="region" and aria-roledescription="carousel".
| Prop | Type | Default | Description |
|---|---|---|---|
slideCount | number | required | Total slides. Needed for snap points and SSR. |
allowMouseDrag | boolean | false | Scroll by dragging with the mouse. |
asChild | boolean | false | Render the child element instead of a div. |
autoplay | boolean | { delay: number } | false | Auto-scroll. Default delay is 4000ms. |
autoSize | boolean | false | Variable-width slides. Item group uses display: flex instead of grid. |
className | string | - | Class names on the root. |
defaultPage | number | 0 | Uncontrolled initial page. |
dir | "ltr" | "rtl" | - | Text direction. Usually inherited from LocaleProvider. |
id | string | - | Unique id for the carousel machine. |
ids | Partial<{ root: string; item: (index: number) => string; itemGroup: string; nextTrigger: string; prevTrigger: string; indicatorGroup: string; indicator: (index: number) => string }> | - | Element ids for composition. |
inViewThreshold | number | number[] | 0.6 | How much of an item must be visible to count as in view. |
loop | boolean | false | Wrap from last page to first. Defaults to true in Zag when autoplay is set and you omit loop. |
onAutoplayStatusChange | (details: AutoplayStatusDetails) => void | - | Autoplay started or stopped. { type, page, isPlaying }. type is "autoplay.start" | "autoplay" | "autoplay.stop". |
onDragStatusChange | (details: DragStatusDetails) => void | - | Drag started or ended. { type, page, isDragging }. type is "dragging.start" | "dragging" | "dragging.end". |
onPageChange | (details: PageChangeDetails) => void | - | Page changed. { page, pageSnapPoint }. |
orientation | "horizontal" | "vertical" | "horizontal" | Scroll axis. |
padding | string | - | Extra space around the scrollport so neighbors stay partly visible (scroll-padding / padding on the track). |
page | number | - | Controlled page. |
slidesPerMove | number | "auto" | "auto" | Slides to move per navigation. auto follows slidesPerPage. |
slidesPerPage | number | 1 | Slides visible at once. Can be fractional (for example 1.5). |
snapType | "proximity" | "mandatory" | "mandatory" | CSS scroll-snap type. |
spacing | string | "16px" | Gap between items. |
translations | IntlTranslations | see below | Localized strings for triggers, items, indicators, autoplay, and progress. |
Default translations:
| Key | Default |
|---|---|
nextTrigger | "Next slide" |
prevTrigger | "Previous slide" |
indicator | "Go to slide {n}" |
item | "{n} of {count}" |
autoplayStart | "Start slide rotation" |
autoplayStop | "Stop slide rotation" |
progressText | "{page} / {totalPages}" |
shadcn.io prev/next buttons also set aria-label to "Previous slide" / "Next slide". Override those labels on the trigger if you localize via translations.
| Attribute | Description |
|---|---|
data-slot | carousel |
data-scope | carousel |
data-part | root |
data-orientation | "horizontal" or "vertical" |
| CSS variable | Description |
|---|---|
--slides-per-page | Visible slides per page |
--slide-spacing | Gap between slides |
--slide-item-size | Computed slide size |
CarouselRootProvider
Takes the API from useCarousel. Renders a div with the same root attributes as Carousel.
| Prop | Type | Default | Description |
|---|---|---|---|
value | UseCarouselReturn | required | Return value of useCarousel(). |
asChild | boolean | false | Render the child element instead of a div. |
className | string | - | Class names on the root. |
Pass slideCount, spacing, autoplay, orientation, and other machine options to useCarousel(), not to CarouselRootProvider.
| Attribute | Description |
|---|---|
data-slot | carousel |
data-scope | carousel |
data-part | root |
data-orientation | "horizontal" or "vertical" |
CarouselContent
Item group (the track). Renders a div. aria-live is "polite" while idle and "off" while autoplay is running.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the track. |
| Attribute | Description |
|---|---|
data-slot | carousel-group |
data-scope | carousel |
data-part | item-group |
data-orientation | "horizontal" or "vertical" |
data-dragging | Present while dragging |
CarouselItem
One slide. Renders a div with role="group" and aria-roledescription="slide". index is required.
| Prop | Type | Default | Description |
|---|---|---|---|
index | number | required | Slide index. |
snapAlign | "start" | "center" | "end" | "start" | Where the item snaps. Ignored when this index is not a snap point. |
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the item. Use w-auto (and a sized child) with autoSize. |
| Attribute | Description |
|---|---|
data-slot | carousel-item |
data-scope | carousel |
data-part | item |
data-index | Slide index |
data-inview | Present when in the viewport |
data-orientation | "horizontal" or "vertical" |
aria-hidden is set when the item is not in view. aria-label defaults to "{n} of {count}".
CarouselControl
Layout wrapper for prev/next (and optional autoplay). Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the control. |
| Attribute | Description |
|---|---|
data-slot | carousel-control |
data-scope | carousel |
data-part | control |
data-orientation | "horizontal" or "vertical" |
CarouselPrevious / CarouselNext
Icon buttons. Always asChild onto Button (size="icon-md", variant="outline", pill). Default aria-label is "Previous slide" / "Next slide". Chevrons are aria-hidden. Extra children are not rendered.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Position/offset of the trigger. |
asChild | boolean | true | Always merged onto the inner Button. |
| Attribute | Description |
|---|---|
data-slot | carousel-previous / carousel-next |
data-scope | carousel |
data-part | prev-trigger / next-trigger |
data-orientation | "horizontal" or "vertical" |
aria-controls points at the item group. The button is disabled when that direction cannot scroll (loop keeps both enabled).
CarouselIndicatorGroup
Container for dots or thumbnails. Renders a div. Arrow keys on this group change the page.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the group. |
| Attribute | Description |
|---|---|
data-slot | carousel-indicator-group |
data-scope | carousel |
data-part | indicator-group |
data-orientation | "horizontal" or "vertical" |
CarouselIndicator
One page control. Renders a button. index is required.
| Prop | Type | Default | Description |
|---|---|---|---|
index | number | required | Page index (use pageSnapPoints when slidesPerPage is not 1). |
readOnly | boolean | false | Display-only; not clickable. |
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the indicator. |
| Attribute | Description |
|---|---|
data-slot | carousel-indicator |
data-scope | carousel |
data-part | indicator |
data-current | Present for the active page |
data-index | Indicator index |
data-readonly | Present when read-only |
data-orientation | "horizontal" or "vertical" |
Default aria-label is "Go to slide {n}".
CarouselAutoplayTrigger
Toggles autoplay. Always asChild onto Button unless you pass your own child. Default control is an icon button with play/pause icons.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | true | Merge onto a single child. |
className | string | - | Class names on the trigger. |
| Attribute | Description |
|---|---|
data-slot | carousel-autoplay-trigger |
data-scope | carousel |
data-part | autoplay-trigger |
data-orientation | "horizontal" or "vertical" |
data-pressed | Present when autoplay is on |
Zag sets aria-label to "Stop slide rotation" / "Start slide rotation". The default shadcn.io button also sets aria-label="Toggle autoplay".
CarouselAutoplayIndicator
Shows its children while autoplay is playing and fallback while paused. Renders a span.
| Prop | Type | Default | Description |
|---|---|---|---|
fallback | ReactNode | - | Content when autoplay is paused. |
asChild | boolean | false | Merge onto a single child. |
| Attribute | Description |
|---|---|
data-slot | carousel-autoplay-indicator |
data-scope | carousel |
data-part | autoplay-indicator |
CarouselProgressText
"current / total" page text. Renders a span. Pass children to replace the default label.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the text. |
| Attribute | Description |
|---|---|
data-slot | carousel-progress-text |
data-scope | carousel |
data-part | progress-text |
useCarousel
Creates the carousel API for CarouselRootProvider. Accepts the same machine options as Carousel except layout-only props (asChild, className).
const carousel = useCarousel({ slideCount: 5, spacing: "16px" });
carousel.scrollToIndex(3);CarouselContext / useCarouselContext
Render-prop or hook access. Use inside Carousel or CarouselRootProvider.
| Property | Type | Description |
|---|---|---|
page | number | Current page. |
pageSnapPoints | number[] | Snap offsets (one per page). |
isPlaying | boolean | Whether autoplay is running. |
isDragging | boolean | Whether the pointer is dragging (allowMouseDrag). |
canScrollNext / canScrollPrev | boolean | Whether navigation in that direction is possible. |
scrollToIndex | (index: number, instant?: boolean) => void | Jump to an item. |
scrollTo | (page: number, instant?: boolean) => void | Jump to a page. |
scrollNext / scrollPrev | (instant?: boolean) => void | Move one page. |
getProgress | () => number | page / pageSnapPoints.length (0 on the first page). |
getProgressText | () => string | Localized progress label from translations.progressText. |
play / pause | () => void | Control autoplay. |
isInView | (index: number) => boolean | Whether an item is in view. |
refresh | () => void | Recompute snap points (after slide count or size changes). |
CarouselContext children: (context) => ReactNode.
Accessibility
Complies with the Carousel WAI-ARIA design pattern.
The root is a labeled region (role="region", aria-roledescription="carousel"). Each item is a slide (role="group", aria-roledescription="slide"). Off-screen slides are aria-hidden. The track uses aria-live="polite" unless autoplay is playing (aria-live="off").
Prev/next and autoplay are named buttons. Decorative chevrons and play/pause icons are aria-hidden. Prefer loop plus a pause control (CarouselAutoplayTrigger or pause() / play()) when using autoplay. Honor reduced motion: avoid autoplay or use a long delay.
Images need alt. Thumbnail indicators that repeat the same image can use alt="" if the control has an accessible name.
Keyboard support
| Key | Description |
|---|---|
Tab | Move to the next control (prev, next, autoplay, or an indicator). |
Shift + Tab | Move to the previous control. |
Enter / Space | Activate the focused trigger or indicator. |
ArrowRight | Next page when focus is in the indicator group (horizontal). Respects RTL. |
ArrowLeft | Previous page (horizontal indicators). |
ArrowDown | Next page (vertical indicators). |
ArrowUp | Previous page (vertical indicators). |
Home | First page (indicator group). |
End | Last page (indicator group). |
Touch swipe always moves the track. Mouse drag requires allowMouseDrag.