Carousel
A shadcn-style carousel component built with Ark UI primitives.
import {
Carousel,
CarouselContent,
CarouselControl,
CarouselIndicatorGroup,
CarouselIndicatorItem,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const SLIDES = ["Slide 1", "Slide 2", "Slide 3"] as const;
const CarouselDemo = () => (
<Carousel className="mx-auto w-full max-w-md" slideCount={SLIDES.length}>
<CarouselControl className="relative h-40 rounded-lg border border-border bg-muted/30">
<CarouselContent>
{SLIDES.map((label, index) => (
<CarouselItem
className="flex h-40 items-center justify-center font-medium text-lg"
index={index}
key={label}
>
{label}
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious anchorButtons />
<CarouselNext anchorButtons />
</CarouselControl>
<CarouselIndicatorGroup>
{SLIDES.map((label, index) => (
<CarouselIndicatorItem index={index} key={label} />
))}
</CarouselIndicatorGroup>
</Carousel>
);
export default CarouselDemo;
Installation
npx shadcn@latest add @ark-cn/carouselInstall the dependency required by this primitive:
npm install @ark-ui/react lucide-react class-variance-authorityCopy the component source into your app:
TSXcomponents/ui/carousel.tsx
"use client";
import { Carousel as CarouselPrimitive } from "@ark-ui/react/carousel";
import type { VariantProps } from "class-variance-authority";
import {
ChevronLeftIcon,
ChevronRightIcon,
PauseIcon,
PlayIcon,
} from "lucide-react";
import { Button, type buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export const Carousel = ({ ...props }: CarouselPrimitive.RootProps) => {
return <CarouselPrimitive.Root {...props} />;
};
export const CarouselControl = ({
className,
...props
}: CarouselPrimitive.ControlProps) => {
return (
<CarouselPrimitive.Control
className={cn("relative", className)}
{...props}
/>
);
};
export const CarouselPrevious = ({
className,
variant = "outline",
size = "icon-sm",
anchorButtons = false,
...props
}: CarouselPrimitive.PrevTriggerProps & {
variant?: VariantProps<typeof buttonVariants>["variant"];
size?: VariantProps<typeof buttonVariants>["size"];
anchorButtons?: boolean;
}) => {
return (
<CarouselPrimitive.PrevTrigger {...props} asChild>
<Button
variant={variant}
size={size}
className={cn(
"data-[orientation='vertical']:rotate-90",
anchorButtons &&
"absolute touch-manipulation rounded-full before:absolute before:inset-0 before:rounded-full",
anchorButtons &&
"data-[orientation='horizontal']:top-1/2 data-[orientation='horizontal']:-left-12 data-[orientation='horizontal']:-translate-y-1/2",
anchorButtons &&
"data-[orientation='vertical']:-top-12 data-[orientation='vertical']:left-1/2 data-[orientation='vertical']:-translate-x-1/2 ",
className,
)}
>
<ChevronLeftIcon />
<span className="sr-only">Previous sli de</span>
</Button>
</CarouselPrimitive.PrevTrigger>
);
};
export const CarouselNext = ({
className,
variant = "outline",
size = "icon-sm",
anchorButtons = false,
...props
}: CarouselPrimitive.NextTriggerProps & {
variant?: VariantProps<typeof buttonVariants>["variant"];
size?: VariantProps<typeof buttonVariants>["size"];
anchorButtons?: boolean;
}) => {
return (
<CarouselPrimitive.NextTrigger {...props} asChild>
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"data-[orientation='vertical']:rotate-90",
anchorButtons &&
"absolute touch-manipulation rounded-full before:content-[''] before:absolute before:inset-0 before:rounded-full",
anchorButtons &&
"data-[orientation='horizontal']:top-1/2 data-[orientation='horizontal']:-right-12 data-[orientation='horizontal']:-translate-y-1/2",
anchorButtons &&
"data-[orientation='vertical']:-bottom-12 data-[orientation='vertical']:left-1/2 data-[orientation='vertical']:-translate-x-1/2 ",
className,
)}
>
<ChevronRightIcon />
<span className="sr-only">Next slide</span>
</Button>
</CarouselPrimitive.NextTrigger>
);
};
export const CarouselContent = ({
className,
...props
}: CarouselPrimitive.ItemGroupProps) => {
return (
<CarouselPrimitive.ItemGroup
className={cn("w-full h-full", className)}
{...props}
/>
);
};
export const CarouselItem = ({
children,
...props
}: CarouselPrimitive.ItemProps) => {
return <CarouselPrimitive.Item {...props}>{children}</CarouselPrimitive.Item>;
};
export const CarouselIndicatorGroup = ({
className,
...props
}: CarouselPrimitive.IndicatorGroupProps) => {
return (
<CarouselPrimitive.IndicatorGroup
className={cn(
"flex items-center justify-center data-[orientation='vertical']:flex-col gap-2 p-5",
className,
)}
{...props}
/>
);
};
export const CarouselIndicatorItem = ({
className,
...props
}: CarouselPrimitive.IndicatorProps) => {
return (
<CarouselPrimitive.Indicator
className={cn(
"cursor-pointer disabled:cursor-default w-2 h-2 rounded-full bg-accent data-current:bg-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-ring",
className,
)}
{...props}
/>
);
};
export const CarouselAutoPlayIndicator = CarouselPrimitive.AutoplayIndicator;
export const CarouselAutoplay = ({
className,
variant = "outline",
size = "icon-sm",
children,
...props
}: CarouselPrimitive.AutoplayTriggerProps & {
variant?: VariantProps<typeof buttonVariants>["variant"];
size?: VariantProps<typeof buttonVariants>["size"];
}) => {
return (
<CarouselPrimitive.AutoplayTrigger {...props} asChild>
<Button
variant={variant}
size={size}
className={cn(className)}
{...props}
>
{children || (
<CarouselPrimitive.AutoplayIndicator fallback={<PlayIcon />}>
<PauseIcon />
</CarouselPrimitive.AutoplayIndicator>
)}
<span className="sr-only">Autoplay</span>
</Button>
</CarouselPrimitive.AutoplayTrigger>
);
};
export const CarouselContext = CarouselPrimitive.Context;
Update import aliases to match your project setup.
Usage
import * as Carousel from "@/components/ui/carousel"Read exported parts in src/components/ui/carousel.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Basic carousel
import {
Carousel,
CarouselContent,
CarouselControl,
CarouselIndicatorGroup,
CarouselIndicatorItem,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const SLIDES = ["Slide 1", "Slide 2", "Slide 3"] as const;
const CarouselDemo = () => (
<Carousel className="mx-auto w-full max-w-md" slideCount={SLIDES.length}>
<CarouselControl className="relative h-40 rounded-lg border border-border bg-muted/30">
<CarouselContent>
{SLIDES.map((label, index) => (
<CarouselItem
className="flex h-40 items-center justify-center font-medium text-lg"
index={index}
key={label}
>
{label}
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious anchorButtons />
<CarouselNext anchorButtons />
</CarouselControl>
<CarouselIndicatorGroup>
{SLIDES.map((label, index) => (
<CarouselIndicatorItem index={index} key={label} />
))}
</CarouselIndicatorGroup>
</Carousel>
);
export default CarouselDemo;
Autoplay with controls
import {
Carousel,
CarouselAutoplay,
CarouselContent,
CarouselControl,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const SLIDES = ["Slide 1", "Slide 2", "Slide 3"] as const;
const CarouselAutoplayDemo = () => (
<Carousel
className="mx-auto w-full max-w-md"
slideCount={SLIDES.length}
autoplay={{ delay: 2000 }}
>
<CarouselContent className="h-40 rounded-lg border border-border bg-muted/30">
{SLIDES.map((label, index) => (
<CarouselItem
className="flex h-40 items-center justify-center font-medium text-lg"
index={index}
key={`auto-${label}`}
>
{label}
</CarouselItem>
))}
</CarouselContent>
<CarouselControl className="flex items-center justify-center gap-2 pt-3">
<CarouselPrevious size="icon-sm" />
<CarouselAutoplay size="icon-sm" />
<CarouselNext size="icon-sm" />
</CarouselControl>
</Carousel>
);
export default CarouselAutoplayDemo;
Controlled page
Page 1 of 5
import { useState } from "react";
import {
Carousel,
CarouselContent,
CarouselControl,
CarouselIndicatorGroup,
CarouselIndicatorItem,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const SLIDES = ["Slide 1", "Slide 2", "Slide 3", "Slide 4", "Slide 5"] as const;
const CarouselControlledDemo = () => {
const [page, setPage] = useState(0);
return (
<Carousel
className="mx-auto w-full max-w-md space-y-3"
page={page}
slideCount={SLIDES.length}
onPageChange={(details) => setPage(details.page)}
>
<CarouselControl className="rounded-lg border border-border bg-muted/30">
<CarouselPrevious anchorButtons />
<CarouselContent className="h-40">
{SLIDES.map((label, index) => (
<CarouselItem
className="flex h-full items-center justify-center font-medium text-lg"
index={index}
key={`controlled-${label}`}
>
{label}
</CarouselItem>
))}
</CarouselContent>
<CarouselNext anchorButtons />
</CarouselControl>
<div className="flex items-center justify-center gap-3">
<span className="text-muted-foreground text-sm">
Page {page + 1} of {SLIDES.length}
</span>
</div>
<CarouselIndicatorGroup className="p-0">
{SLIDES.map((label, index) => (
<CarouselIndicatorItem
index={index}
key={`controlled-indicator-${label}`}
/>
))}
</CarouselIndicatorGroup>
</Carousel>
);
};
export default CarouselControlledDemo;
Pause on hover
Autoplay: playing
import {
Carousel,
CarouselContent,
CarouselContext,
CarouselIndicatorGroup,
CarouselIndicatorItem,
CarouselItem,
} from "@/components/ui/carousel";
const SLIDES = ["Slide 1", "Slide 2", "Slide 3", "Slide 4", "Slide 5"] as const;
const CarouselPauseOnHoverDemo = () => (
<Carousel
className="mx-auto w-full max-w-md space-y-3"
autoplay={{ delay: 1800 }}
loop
slideCount={SLIDES.length}
>
<CarouselContext>
{({ isPlaying }) => (
<p className="text-center text-muted-foreground text-sm">
Autoplay: {isPlaying ? "playing" : "paused"}
</p>
)}
</CarouselContext>
<CarouselContext>
{(api) => (
<CarouselContent
className="h-40 rounded-lg border border-border bg-muted/30"
onPointerLeave={() => api.play()}
onPointerOver={() => api.pause()}
>
{SLIDES.map((label, index) => (
<CarouselItem
className="flex h-full items-center justify-center font-medium text-lg"
index={index}
key={`hover-${label}`}
>
{label}
</CarouselItem>
))}
</CarouselContent>
)}
</CarouselContext>
<CarouselIndicatorGroup className="p-0">
{SLIDES.map((label, index) => (
<CarouselIndicatorItem index={index} key={`hover-indicator-${label}`} />
))}
</CarouselIndicatorGroup>
</Carousel>
);
export default CarouselPauseOnHoverDemo;
Vertical carousel
import {
Carousel,
CarouselContent,
CarouselControl,
CarouselIndicatorGroup,
CarouselIndicatorItem,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const SLIDES = [
"Slide 1",
"Slide 2",
"Slide 3",
"Slide 4",
"Slide 5",
"Slide 6",
] as const;
const CarouselVerticalDemo = () => (
<Carousel
className="mx-auto flex w-full max-w-md flex-row h-50 gap-4"
orientation="vertical"
slideCount={SLIDES.length}
>
<CarouselContent className="flex flex-1 rounded-lg border border-border bg-muted/30">
{SLIDES.map((label, index) => (
<CarouselItem
className="flex-[0_0_100%] min-w-0 flex items-center justify-center font-medium text-lg w-full h-full"
index={index}
key={`vertical-${label}`}
snapAlign="end"
>
{label}
</CarouselItem>
))}
</CarouselContent>
<CarouselControl className="flex items-center justify-between gap-2 data-[orientation='vertical']:flex-col">
<CarouselPrevious />
<CarouselIndicatorGroup className="p-0">
{SLIDES.map((label, index) => (
<CarouselIndicatorItem
index={index}
key={`vertical-indicator-${label}`}
/>
))}
</CarouselIndicatorGroup>
<CarouselNext />
</CarouselControl>
</Carousel>
);
export default CarouselVerticalDemo;
Scroll to specific slide
import { Button } from "@/components/ui/button";
import {
Carousel,
CarouselContent,
CarouselContext,
CarouselControl,
CarouselIndicatorGroup,
CarouselIndicatorItem,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const SLIDES = ["Slide 1", "Slide 2", "Slide 3", "Slide 4", "Slide 5"] as const;
const CarouselScrollToDemo = () => (
<Carousel
className="mx-auto w-full max-w-md space-y-3"
slideCount={SLIDES.length}
>
<CarouselContext>
{(api) => (
<Button
className="w-fit"
onClick={() => api.scrollToIndex(3)}
type="button"
variant="outline"
>
Go to slide 4
</Button>
)}
</CarouselContext>
<CarouselContent className="h-40 rounded-lg border border-border bg-muted/30">
{SLIDES.map((label, index) => (
<CarouselItem
className="flex h-full items-center justify-center font-medium text-lg"
index={index}
key={`scroll-${label}`}
>
{label}
</CarouselItem>
))}
</CarouselContent>
<CarouselControl className="flex items-center justify-center gap-2">
<CarouselPrevious />
<CarouselNext />
</CarouselControl>
<CarouselIndicatorGroup className="p-0">
{SLIDES.map((label, index) => (
<CarouselIndicatorItem
index={index}
key={`scroll-indicator-${label}`}
/>
))}
</CarouselIndicatorGroup>
</Carousel>
);
export default CarouselScrollToDemo;
Dynamic slides
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Carousel,
CarouselContent,
CarouselControl,
CarouselIndicatorGroup,
CarouselIndicatorItem,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const INITIAL_SLIDES = [0, 1, 2, 3, 4] as const;
const CarouselDynamicSlidesDemo = () => {
const [slides, setSlides] = useState<number[]>([...INITIAL_SLIDES]);
const [page, setPage] = useState(0);
const addSlide = () => {
setSlides((prevSlides) => {
const max = prevSlides.length === 0 ? -1 : Math.max(...prevSlides);
return [...prevSlides, max + 1];
});
};
return (
<div className="mx-auto flex w-full max-w-md flex-col gap-4">
<Carousel
className="w-full space-y-3"
page={page}
slideCount={slides.length}
onPageChange={(details) => setPage(details.page)}
>
<CarouselContent className="h-40 rounded-lg border border-border bg-muted/30">
{slides.map((slide, index) => (
<CarouselItem
className="flex h-full items-center justify-center font-medium text-lg"
index={index}
key={`dynamic-${slide}`}
>
Slide {slide + 1}
</CarouselItem>
))}
</CarouselContent>
<CarouselControl className="flex items-center justify-between gap-2">
<CarouselPrevious />
<CarouselIndicatorGroup className="p-0">
{slides.map((slide, index) => (
<CarouselIndicatorItem
index={index}
key={`dynamic-indicator-${slide}`}
/>
))}
</CarouselIndicatorGroup>
<CarouselNext />
</CarouselControl>
</Carousel>
<Button
className="w-fit"
onClick={addSlide}
type="button"
variant="outline"
>
<PlusIcon />
Add Slide
</Button>
</div>
);
};
export default CarouselDynamicSlidesDemo;
Slides per page
import {
Carousel,
CarouselContent,
CarouselContext,
CarouselControl,
CarouselIndicatorGroup,
CarouselIndicatorItem,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const SLIDES = Array.from({ length: 6 }, (_, index) => `Slide ${index + 1}`);
const CarouselSlidesPerPageDemo = () => (
<Carousel
className="mx-auto w-full max-w-md space-y-3"
slideCount={SLIDES.length}
slidesPerPage={2}
spacing="20px"
>
<CarouselControl className="flex items-center justify-center gap-2">
<CarouselPrevious />
<CarouselNext />
</CarouselControl>
<CarouselContent className="h-40">
{SLIDES.map((label, index) => (
<CarouselItem index={index} key={`per-page-${label}`}>
<div className="flex h-40 items-center justify-center rounded-lg border border-border bg-muted/30 font-medium text-lg">
{label}
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselContext>
{(api) => (
<CarouselIndicatorGroup className="p-0">
{api.pageSnapPoints.map((_, index) => (
<CarouselIndicatorItem
index={index}
key={`per-page-indicator-${index}`}
/>
))}
</CarouselIndicatorGroup>
)}
</CarouselContext>
</Carousel>
);
export default CarouselSlidesPerPageDemo;
Custom spacing
spacing="48px"
import {
Carousel,
CarouselContent,
CarouselContext,
CarouselControl,
CarouselIndicatorGroup,
CarouselIndicatorItem,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const SLIDES = Array.from({ length: 6 }, (_, index) => index + 1);
const CarouselSpacingDemo = () => (
<Carousel
className="mx-auto w-full max-w-md space-y-3"
slideCount={SLIDES.length}
slidesPerPage={1.5}
spacing="48px"
>
<p className="text-muted-foreground text-sm">spacing="48px"</p>
<CarouselContent className="h-40">
{SLIDES.map((item, index) => (
<CarouselItem index={index} key={`spacing-${item}`}>
<div className="flex h-40 items-center justify-center rounded-lg border border-border bg-muted/30 font-medium text-lg">
{item}
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselControl className="flex items-center justify-between gap-2">
<CarouselPrevious />
<CarouselContext>
{(api) => (
<CarouselIndicatorGroup className="p-0">
{api.pageSnapPoints.map((_, index) => (
<CarouselIndicatorItem
index={index}
key={`spacing-indicator-${index}`}
/>
))}
</CarouselIndicatorGroup>
)}
</CarouselContext>
<CarouselNext />
</CarouselControl>
</Carousel>
);
export default CarouselSpacingDemo;
Variable size items
import {
Carousel,
CarouselContent,
CarouselContext,
CarouselControl,
CarouselIndicatorGroup,
CarouselIndicatorItem,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const ITEMS = [
{ id: "1", label: "Small", width: "120px" },
{ id: "2", label: "Medium Size", width: "200px" },
{ id: "3", label: "XS", width: "80px" },
{ id: "4", label: "Large Content Here", width: "250px" },
{ id: "5", label: "Regular", width: "150px" },
] as const;
const CarouselVariableSizeDemo = () => (
<Carousel
autoSize
className="mx-auto w-full max-w-md space-y-3"
slideCount={ITEMS.length}
spacing="8px"
>
<CarouselControl className="flex items-center justify-center gap-2">
<CarouselPrevious />
<CarouselNext />
</CarouselControl>
<CarouselContent>
{ITEMS.map((item, index) => (
<CarouselItem index={index} key={item.id} snapAlign="center">
<div
className="flex h-24 items-center justify-center rounded-lg border border-border bg-muted/30 px-3 font-medium text-sm"
style={{ width: item.width }}
>
{item.label}
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselContext>
{(api) => (
<CarouselIndicatorGroup className="p-0">
{api.pageSnapPoints.map((_, index) => (
<CarouselIndicatorItem
index={index}
key={`variable-indicator-${index}`}
/>
))}
</CarouselIndicatorGroup>
)}
</CarouselContext>
</Carousel>
);
export default CarouselVariableSizeDemo;
Thumbnail indicators
import {
Carousel,
CarouselContent,
CarouselControl,
CarouselIndicatorGroup,
CarouselIndicatorItem,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { cn } from "@/lib/utils";
const SLIDES = ["Slide 1", "Slide 2", "Slide 3", "Slide 4", "Slide 5"] as const;
const CarouselThumbnailIndicatorDemo = () => (
<Carousel
className="mx-auto w-full max-w-md space-y-3"
slideCount={SLIDES.length}
>
<CarouselControl className="rounded-lg border border-border bg-muted/30">
<CarouselPrevious anchorButtons />
<CarouselContent className="h-40">
{SLIDES.map((label, index) => (
<CarouselItem
className="flex h-full items-center justify-center font-medium text-lg"
index={index}
key={`thumb-${label}`}
>
{label}
</CarouselItem>
))}
</CarouselContent>
<CarouselNext anchorButtons />
</CarouselControl>
<CarouselIndicatorGroup className="gap-2 p-0">
{SLIDES.map((label, index) => (
<CarouselIndicatorItem
className={cn(
"h-10 w-16 cursor-pointer overflow-hidden rounded border-2 border-transparent bg-muted/40 p-0 text-xs opacity-60 transition-opacity data-current:border-primary data-current:opacity-100 data-current:text-background",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
)}
index={index}
key={`thumb-indicator-${label}`}
>
<div className="flex h-full w-full items-center justify-center">
{index + 1}
</div>
</CarouselIndicatorItem>
))}
</CarouselIndicatorGroup>
</Carousel>
);
export default CarouselThumbnailIndicatorDemo;
API reference
This component mirrors the upstream Ark UI primitive. All props and DOM behavior are defined by Ark unless you see an ark-cn-only row below.
CarouselPrevious
| Prop | Type | Description |
|---|---|---|
| variant? | ButtonVariant | Button style preset (default outline). |
| size? | ButtonSize | Button size (default icon-sm). |
| anchorButtons? | boolean | Positions the control as a floating anchored button outside the carousel. |
CarouselNext
| Prop | Type | Description |
|---|---|---|
| variant? | ButtonVariant | Button style preset (default outline). |
| size? | ButtonSize | Button size (default icon-sm). |
| anchorButtons? | boolean | Positions the control as a floating anchored button outside the carousel. |
CarouselAutoplay
| Prop | Type | Description |
|---|---|---|
| variant? | ButtonVariant | Button style preset (default outline). |
| size? | ButtonSize | Button size (default icon-sm). |
See the ARK UI documentation for the full API.
Accessibility
See the Ark UI documentation for clarification.