Carousel

Image and content slider — supports auto-play, navigation arrows, dot indicators, and smooth slide transitions.

Preview

Basic carousel

import { Carousel, CarouselSlide } from '@/registry/ui-kit/tailwind-carousel/react'

function Example() {
  return (
    <Carousel>
      <CarouselSlide>
        <div className="flex h-48 items-center justify-center bg-indigo-100">
          <span className="text-lg font-semibold text-indigo-600">Slide 1</span>
        </div>
      </CarouselSlide>
      <CarouselSlide>
        <div className="flex h-48 items-center justify-center bg-emerald-100">
          <span className="text-lg font-semibold text-emerald-600">Slide 2</span>
        </div>
      </CarouselSlide>
    </Carousel>
  )
}

Auto-play

import { Carousel, CarouselSlide } from '@/registry/ui-kit/tailwind-carousel/react'

function Example() {
  return (
    <Carousel autoPlay interval={3000}>
      <CarouselSlide><div className="h-48 bg-rose-100" /></CarouselSlide>
      <CarouselSlide><div className="h-48 bg-sky-100" /></CarouselSlide>
      <CarouselSlide><div className="h-48 bg-violet-100" /></CarouselSlide>
    </Carousel>
  )
}

Component API

PropDefaultDescription
autoPlayfalseAutomatically cycle through slides.
interval5000Auto-play interval in milliseconds.
className""Additional CSS classes for the carousel wrapper.
CarouselSlideWrapper for each individual slide.

Source Code

"use client";

import { useState, useEffect, useCallback, Children } from "react";

/* ── CarouselSlide ── */

interface CarouselSlideProps {
  className?: string;
  children: React.ReactNode;
}

function CarouselSlide({ className = "", children }: CarouselSlideProps) {
  return <div className={`min-w-full ${className}`}>{children}</div>;
}

/* ── Carousel ── */

interface CarouselProps {
  autoPlay?: boolean;
  interval?: number;
  className?: string;
  children: React.ReactNode;
}

function Carousel({ autoPlay = false, interval = 5000, className = "", children }: CarouselProps) {
  const slides = Children.toArray(children);
  const count = slides.length;
  const [activeIndex, setActiveIndex] = useState(0);

  const next = useCallback(() => {
    setActiveIndex((prev) => (prev + 1) % count);
  }, [count]);

  const prev = useCallback(() => {
    setActiveIndex((prev) => (prev - 1 + count) % count);
  }, [count]);

  useEffect(() => {
    if (!autoPlay) return;
    const timer = setInterval(next, interval);
    return () => clearInterval(timer);
  }, [autoPlay, interval, next]);

  return (
    <div className={`relative overflow-hidden rounded-lg ${className}`}>
      {/* Slides */}
      <div
        className="flex transition-transform duration-500"
        style={{ transform: `translateX(-${activeIndex * 100}%)` }}
      >
        {slides}
      </div>

      {/* Left arrow */}
      {count > 1 && (
        <button
          type="button"
          onClick={prev}
          className="absolute left-3 top-1/2 -translate-y-1/2 rounded-full bg-black/30 p-1.5 text-white backdrop-blur-sm transition-colors hover:bg-black/50"
          aria-label="Previous slide"
        >
          <svg
            className="h-4 w-4"
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
          >
            <path d="m15 18-6-6 6-6" />
          </svg>
        </button>
      )}

      {/* Right arrow */}
      {count > 1 && (
        <button
          type="button"
          onClick={next}
          className="absolute right-3 top-1/2 -translate-y-1/2 rounded-full bg-black/30 p-1.5 text-white backdrop-blur-sm transition-colors hover:bg-black/50"
          aria-label="Next slide"
        >
          <svg
            className="h-4 w-4"
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
          >
            <path d="m9 18 6-6-6-6" />
          </svg>
        </button>
      )}

      {/* Indicators */}
      {count > 1 && (
        <div className="absolute bottom-3 left-1/2 flex -translate-x-1/2 gap-1.5">
          {slides.map((_, i) => (
            <button
              key={i}
              type="button"
              onClick={() => setActiveIndex(i)}
              className={`h-2 w-2 rounded-full transition-colors ${
                i === activeIndex ? "bg-white" : "bg-white/50"
              }`}
              aria-label={`Go to slide ${i + 1}`}
            />
          ))}
        </div>
      )}
    </div>
  );
}

export { Carousel, CarouselSlide };
export default Carousel;