The Future of Web Dev
The Future of Web Dev
Copy-Paste React Carousel with Tailwind and shadcn/ui – Carouselcn
A composable carousel component for React with loop, autoplay, drag, keyboard navigation, and controlled mode.

carouselcn is a React carousel component that allows you to create customizable carousel sliders with Tailwind CSS and shadcn/ui.
The architecture is composable. Navigation, dot indicators, and autoplay are separate sub‑components you can combine or swap out.
Hooks like useCarousel and useCarouselAutoplay expose internal state for building custom controls, while keyboard events and ARIA labels keep the carousel usable without a mouse.
Features
🧩 Composable Structure: Mix root, content, item, navigation, indicator, and autoplay parts.
↔️ Two Orientations: Render horizontal or vertical carousels.
🔁 Loop Mode: Move from the final slide back to the first slide.
👆 Drag And Swipe: Support mouse drag and touch swipe gestures.
🧱 Multi Item Layouts: Show multiple slides with flexible sizing.
▶️ Autoplay: Start, pause, resume, or toggle automatic slide movement.
🎛️ Custom Controls: Build custom navigation and indicators from carousel state.
♿ Accessible: Add keyboard navigation and ARIA labels for carousel controls.
Use Cases
- Build product image carousels for ecommerce pages.
- Add testimonial sliders to marketing sections.
- Create feature showcases for SaaS landing pages.
- Display card collections inside dashboards.
How to Use It
Table Of Contents
Prerequisites
You need a React project with Tailwind CSS already configured. The component also depends on lucide-react for the navigation icons and a cn utility function for class merging. Both are standard on any shadcn/ui project.
Option 1: CLI Install
Run the shadcn CLI to add the component directly to your project:
npx shadcn@latest add carouselOption 2: Manual Copy
Copy the component code from the carouselcn documentation and paste it into your project at components/ui/carousel.tsx.
If your project does not yet have the cn utility, create lib/utils.ts:
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Then install the required packages:
npm install clsx tailwind-merge lucide-reactBasic Carousel
A minimal setup requires Carousel as the root, CarouselContent as the slide container, and one or more CarouselItem elements. CarouselNavigation adds prev/next buttons. CarouselIndicator adds dot indicators.
"use client";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNavigation,
CarouselIndicator,
} from "@/components/ui/carousel";
export default function BasicCarousel() {
return (
<div className="relative w-full max-w-sm">
<Carousel>
<CarouselContent>
{["A", "B", "C", "D"].map((label) => (
<CarouselItem key={label} className="p-4">
<div className="flex aspect-square items-center justify-center rounded-lg border bg-card">
<span className="text-4xl font-semibold">{label}</span>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselNavigation />
<CarouselIndicator />
</Carousel>
</div>
);
}Vertical Carousel
Pass orientation="vertical" to the root Carousel component. Set a fixed height on CarouselContent so slides stack vertically within the container.
<Carousel orientation="vertical">
<CarouselContent className="h-64">
<CarouselItem>Slide 1</CarouselItem>
<CarouselItem>Slide 2</CarouselItem>
<CarouselItem>Slide 3</CarouselItem>
</CarouselContent>
<CarouselNavigation />
</Carousel>Loop Mode
Set loop={true} on the root component. After the final slide, the carousel wraps back to the first.
<Carousel loop>
<CarouselContent>
<CarouselItem>Slide 1</CarouselItem>
<CarouselItem>Slide 2</CarouselItem>
<CarouselItem>Slide 3</CarouselItem>
</CarouselContent>
<CarouselNavigation />
</Carousel>Autoplay
Call useCarouselAutoplay inside a component rendered within a Carousel. The hook returns play, pause, and toggle functions along with the current isPlaying state.
"use client";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNavigation,
useCarouselAutoplay,
} from "@/components/ui/carousel";
function AutoplayControls() {
const { isPlaying, toggle } = useCarouselAutoplay({
interval: 3000,
autoStart: true,
});
return (
<button onClick={toggle} className="mt-2 text-sm">
{isPlaying ? "Pause" : "Play"}
</button>
);
}
export default function AutoplayCarousel() {
return (
<Carousel loop>
<CarouselContent>
<CarouselItem>Slide 1</CarouselItem>
<CarouselItem>Slide 2</CarouselItem>
<CarouselItem>Slide 3</CarouselItem>
</CarouselContent>
<CarouselNavigation />
<AutoplayControls />
</Carousel>
);
}Controlled Mode
Pass index and onIndexChange to Carousel to drive the active slide from your own state. This is useful when navigation lives outside the carousel, such as a thumbnail strip or a step indicator.
"use client";
import { useState } from "react";
import {
Carousel,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
export default function ControlledCarousel() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<div>
<Carousel index={activeIndex} onIndexChange={setActiveIndex}>
<CarouselContent>
<CarouselItem>Step 1</CarouselItem>
<CarouselItem>Step 2</CarouselItem>
<CarouselItem>Step 3</CarouselItem>
</CarouselContent>
</Carousel>
<div className="mt-4 flex gap-2">
{[0, 1, 2].map((i) => (
<button
key={i}
onClick={() => setActiveIndex(i)}
className={`rounded px-3 py-1 text-sm ${
activeIndex === i ? "bg-black text-white" : "bg-gray-100"
}`}
>
Step {i + 1}
</button>
))}
</div>
</div>
);
}Custom Transition Speed
Pass a transition object to CarouselContent to control animation duration and easing.
<CarouselContent transition={{ duration: 500, ease: "ease-in-out" }}>
<CarouselItem>Slide 1</CarouselItem>
<CarouselItem>Slide 2</CarouselItem>
</CarouselContent>Reading State with useCarousel
Call useCarousel inside any component rendered within a Carousel to read the current index or total count. You can also call setIndex to advance programmatically.
function SlideCounter() {
const { index, itemsCount } = useCarousel();
return (
<p className="text-sm text-muted-foreground">
{index + 1} / {itemsCount}
</p>
);
}Multi-Item Display
Control how many slides appear simultaneously through Tailwind’s basis-* classes on CarouselItem. A class of basis-1/3 places three slides in the viewport at once.
<Carousel>
<CarouselContent>
{items.map((item) => (
<CarouselItem key={item.id} className="basis-1/3 p-2">
<div className="rounded-lg border p-4">{item.title}</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselNavigation alwaysShow />
</Carousel>API Reference
Carousel
Root component that holds context for all sub-components.
| Prop | Type | Description |
|---|---|---|
initialIndex | number | Starting slide index in uncontrolled mode. |
index | number | Active slide index in controlled mode. |
onIndexChange | (index: number) => void | Fires whenever the active slide changes. |
disableDrag | boolean | Turns off mouse drag and touch swipe. |
loop | boolean | Wraps from the last slide to the first. |
orientation | 'horizontal' | 'vertical' | Scroll axis for slide movement. |
className | string | Additional class names for the root element. |
CarouselContent
Container for all CarouselItem children. Handles drag interaction and the CSS slide transition.
| Prop | Type | Description |
|---|---|---|
transition | { duration?: number; ease?: string } | Animation duration in milliseconds and CSS easing function. |
className | string | Additional class names. |
CarouselItem
Wrapper for an individual slide.
| Prop | Type | Description |
|---|---|---|
className | string | Class names for sizing, spacing, and layout. |
CarouselNavigation
Renders previous and next arrow buttons.
| Prop | Type | Description |
|---|---|---|
alwaysShow | boolean | Keeps buttons visible even at the first or last slide. |
className | string | Class names for the navigation wrapper. |
classNameButton | string | Class names for each individual button. |
CarouselIndicator
Renders a dot indicator for each slide.
| Prop | Type | Description |
|---|---|---|
className | string | Class names for the indicator wrapper. |
classNameButton | string | Class names for each dot button. |
useCarousel
Hook for reading and setting carousel state. Must be called inside a component rendered within a Carousel.
const { index, setIndex, itemsCount } = useCarousel();| Value | Type | Description |
|---|---|---|
index | number | Current active slide index. |
setIndex | (index: number) => void | Sets the active slide programmatically. |
itemsCount | number | Total number of slides. |
useCarouselAutoplay
Hook for automatic slide advancement. Must be used inside a Carousel.
const { isPlaying, play, pause, toggle } = useCarouselAutoplay({
interval: 3000,
autoStart: true,
});Options:
| Option | Type | Description |
|---|---|---|
interval | number | Time in milliseconds between slide advances. |
autoStart | boolean | Starts playback immediately on mount when true. |
Return values:
| Value | Type | Description |
|---|---|---|
isPlaying | boolean | Current playback state. |
play | () => void | Starts autoplay. |
pause | () => void | Stops autoplay. |
toggle | () => void | Switches between playing and paused. |
Related Resources
- shadcn/ui: Copy-paste component library built on Radix UI and Tailwind CSS.
- Embla Carousel: A low-level carousel engine for React and vanilla JS with precise touch event handling.
- Swiper: Full-featured slider library with React bindings covering 3D effects, thumbs, and grid layouts.
FAQs
Q: Can I use carouselcn in a Next.js App Router project?
A: Yes. Add the "use client" directive at the top of any file that imports these components.
Q: Does carouselcn require a full shadcn/ui setup?
A: No. The components run in any React project with Tailwind CSS. The actual requirements are clsx, tailwind-merge, and lucide-react.
Q: How do I disable drag on touch but keep it on desktop?
A: The disableDrag prop on Carousel turns off both mouse and touch interaction globally. Selective disabling by input type is not a built-in option. You would need to add a custom onPointerDown check inside the component file directly.
Q: Can I replace the CSS transition with a custom animation approach?
A: The transition prop on CarouselContent accepts a duration and a CSS easing string. You can replace the CSS transition logic with any animation approach you prefer by editing carousel.tsx directly.
Q: Does loop mode work correctly when multiple slides are visible at once?
A: Yes. Loop behavior tracks the active index, not the number of visible slides. Setting loop={true} wraps the index correctly regardless of how many items appear in the viewport simultaneously.
