import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const slides = [1, 2, 3, 4, 5];
export default function CarouselBasic() {
return (
<Carousel className="w-full max-w-sm">
<CarouselContent>
{slides.map((slide) => (
<CarouselItem key={slide} aria-label={`${slide} of ${slides.length}`}>
<div className="flex aspect-[3/2] items-center justify-center rounded-lg bg-layer-2 text-3xl font-medium text-muted-foreground shadow-[inset_0_0_0_0.5px_var(--border)]">
{slide}
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
);
}- Add the registryOnce per project
pnpm dlx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json - Add carousel
pnpm dlx shadcn@latest add @jarvis/carousel
Usage
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
export function ScreenshotCarousel() {
return (
<Carousel>
<CarouselContent>
<CarouselItem>One</CarouselItem>
<CarouselItem>Two</CarouselItem>
<CarouselItem>Three</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
);
}The carousel uses native scrolling with CSS scroll snapping, so it needs no extra packages. The arrow keys move it one slide at a time while focus is inside it.
Each slide fills the carousel. Give CarouselItem a basis-* class to show more than one, and set
--carousel-gap on the carousel to change the space between them.
Give each slide an aria-label such as "2 of 5" so screen readers can say where they are.
Composition
Examples
Several in view
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { gradientImage } from "@/examples/data";
const covers = [
{ title: "Blue and green", image: gradientImage("#7698fd", "#6bd586", 160, 160) },
{ title: "Yellow and red", image: gradientImage("#f2cf76", "#f17471", 160, 160) },
{ title: "Light and dark blue", image: gradientImage("#a2bcff", "#3b5cf6", 160, 160) },
{ title: "Pale and deep green", image: gradientImage("#b8e9c1", "#198b43", 160, 160) },
{ title: "Cream and gold", image: gradientImage("#f7e5b5", "#cb9f34", 160, 160) },
{ title: "Pink and crimson", image: gradientImage("#f2bbb7", "#b82d35", 160, 160) },
];
export default function CarouselSeveral() {
return (
<Carousel className="w-full max-w-xl">
<CarouselContent>
{covers.map((cover, index) => (
<CarouselItem
key={cover.title}
className="basis-1/2 sm:basis-1/3"
aria-label={`${index + 1} of ${covers.length}`}
>
<img src={cover.image} alt={cover.title} className="aspect-square w-full rounded-lg" />
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
);
}Dots
CarouselDots adds a dot for each position.
import { Carousel, CarouselContent, CarouselDots, CarouselItem } from "@/components/ui/carousel";
import { gradientImage } from "@/examples/data";
const photos = [
{ alt: "Blue and green", image: gradientImage("#7698fd", "#6bd586", 480, 320) },
{ alt: "Yellow and red", image: gradientImage("#f2cf76", "#f17471", 480, 320) },
{ alt: "Light and dark blue", image: gradientImage("#a2bcff", "#3b5cf6", 480, 320) },
{ alt: "Pale and deep green", image: gradientImage("#b8e9c1", "#198b43", 480, 320) },
];
export default function CarouselWithDots() {
return (
<Carousel className="w-full max-w-sm">
<CarouselContent>
{photos.map((photo, index) => (
<CarouselItem key={photo.alt} aria-label={`${index + 1} of ${photos.length}`}>
<img src={photo.image} alt={photo.alt} className="aspect-[3/2] w-full rounded-lg" />
</CarouselItem>
))}
</CarouselContent>
<CarouselDots className="absolute inset-x-0 bottom-3 *:bg-white/50 *:data-active:bg-white" />
</Carousel>
);
}Controls below
Placed directly inside the carousel, CarouselPrevious and CarouselNext sit on either side of
the slides. Put them in another element to lay them out yourself, such as in a row with the dots.
Invite your team
Add people from Settings, then Members.
Connect a repository
Link GitHub to see commits on tasks.
Set up alerts
Choose which updates reach your inbox.
import {
Carousel,
CarouselContent,
CarouselDots,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const tips = [
{ title: "Invite your team", body: "Add people from Settings, then Members." },
{ title: "Connect a repository", body: "Link GitHub to see commits on tasks." },
{ title: "Set up alerts", body: "Choose which updates reach your inbox." },
];
export default function CarouselControls() {
return (
<Carousel className="w-full max-w-sm">
<CarouselContent>
{tips.map((tip, index) => (
<CarouselItem key={tip.title} aria-label={`${index + 1} of ${tips.length}`}>
<div className="flex h-full flex-col gap-1 rounded-lg bg-layer-1 p-4 shadow-[inset_0_0_0_0.5px_var(--border-muted)]">
<p className="text-sm font-medium">{tip.title}</p>
<p className="text-sm text-muted-foreground">{tip.body}</p>
</div>
</CarouselItem>
))}
</CarouselContent>
<div className="mt-3 flex items-center justify-between">
<CarouselDots />
<div className="flex gap-2">
<CarouselPrevious />
<CarouselNext />
</div>
</div>
</Carousel>
);
}Vertical
Set orientation="vertical" to scroll up and down, and give CarouselContent a height.
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const slides = [1, 2, 3, 4, 5];
export default function CarouselVertical() {
return (
<Carousel orientation="vertical" className="w-full max-w-xs">
<CarouselContent className="h-56">
{slides.map((slide) => (
<CarouselItem
key={slide}
className="basis-1/2"
aria-label={`${slide} of ${slides.length}`}
>
<div className="flex h-full items-center justify-center rounded-lg bg-layer-2 text-2xl font-medium text-muted-foreground shadow-[inset_0_0_0_0.5px_var(--border)]">
{slide}
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
);
}Props
useCarousel() gives your own parts inside the carousel its state and controls: index, count,
canScrollPrev, canScrollNext, scrollTo(index), scrollPrev() and scrollNext().
| Part | Extends |
|---|---|
| Carousel | <div> |
| CarouselContent | <div> |
| CarouselItem | <div> |
| CarouselPrevious | Button |
| CarouselNext | Button |
| CarouselDots | <div> |
Carousel
| Prop | Type | Default |
|---|---|---|
orientationThe direction the slides scroll in. Vertical carousels need a height on | "horizontal" | "vertical" | "horizontal" |
CarouselPrevious
| Prop | Type | Default |
|---|---|---|
focusableWhenDisabledWhether the button should be focusable when disabled. | boolean | false |
nativeButtonWhether the component renders a native | boolean | true |
variant | "destructive" | "ghost" | "link" | "outline" | "primary" | "secondary" | "secondary" |
size | "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | "icon-sm" |
CarouselNext
| Prop | Type | Default |
|---|---|---|
focusableWhenDisabledWhether the button should be focusable when disabled. | boolean | false |
nativeButtonWhether the component renders a native | boolean | true |
variant | "destructive" | "ghost" | "link" | "outline" | "primary" | "secondary" | "secondary" |
size | "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | "icon-sm" |