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

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 carousel

Option 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-react

Basic 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.

PropTypeDescription
initialIndexnumberStarting slide index in uncontrolled mode.
indexnumberActive slide index in controlled mode.
onIndexChange(index: number) => voidFires whenever the active slide changes.
disableDragbooleanTurns off mouse drag and touch swipe.
loopbooleanWraps from the last slide to the first.
orientation'horizontal' | 'vertical'Scroll axis for slide movement.
classNamestringAdditional class names for the root element.

CarouselContent

Container for all CarouselItem children. Handles drag interaction and the CSS slide transition.

PropTypeDescription
transition{ duration?: number; ease?: string }Animation duration in milliseconds and CSS easing function.
classNamestringAdditional class names.

CarouselItem

Wrapper for an individual slide.

PropTypeDescription
classNamestringClass names for sizing, spacing, and layout.

CarouselNavigation

Renders previous and next arrow buttons.

PropTypeDescription
alwaysShowbooleanKeeps buttons visible even at the first or last slide.
classNamestringClass names for the navigation wrapper.
classNameButtonstringClass names for each individual button.

CarouselIndicator

Renders a dot indicator for each slide.

PropTypeDescription
classNamestringClass names for the indicator wrapper.
classNameButtonstringClass 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();
ValueTypeDescription
indexnumberCurrent active slide index.
setIndex(index: number) => voidSets the active slide programmatically.
itemsCountnumberTotal 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:

OptionTypeDescription
intervalnumberTime in milliseconds between slide advances.
autoStartbooleanStarts playback immediately on mount when true.

Return values:

ValueTypeDescription
isPlayingbooleanCurrent playback state.
play() => voidStarts autoplay.
pause() => voidStops autoplay.
toggle() => voidSwitches 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.

Marcello Novelli

Marcello Novelli

Design Engineer | Developer | Entrepreneur

Leave a Reply

Your email address will not be published. Required fields are marked *