Carousel

Display content in a horizontally scrolling container with automatic or manual navigation. Supports autoplay, keyboard controls, and touch gestures for mobile.

Basic carousel

A simple content slider with automatic advancement and manual controls. Pauses on hover to allow reading.

Mountain Adventure

Slide content here

Ocean Exploration

Slide content here

Forest Journey

Slide content here

Use cases

Hero banner

Full-width promotional sections at the top of pages

Product showcase

Display multiple products or features in limited space

Image gallery

Browse through a collection of images or screenshots

Testimonials

Rotate customer quotes and success stories

Anatomy

Slide ContentCONTROLSINDICATORS

Interaction states

StateAppearanceNotes
IdleAuto-advances after intervalDefault state; timer running
HoverAuto-advance pausedAllows reading content without rushing
Manual navigationSlide transitions with animationUser clicks prev/next or indicators
Keyboard activeArrow keys navigate slidesFocus must be within carousel region

Usage guidelines

✓ Do
  • ·Provide manual controls in addition to autoplay
  • ·Pause on hover to allow reading content
  • ·Include visible indicators showing current position
  • ·Make controls large enough for touch targets (44px minimum)
✗ Don't
  • ·Don't use carousels for critical content (users may miss it)
  • ·Avoid auto-rotating too quickly (minimum 5s per slide)
  • ·Don't rely on autoplay alone — always provide manual navigation
  • ·Avoid excessive slides (3–5 is usually optimal)

Props

PropTypeDefaultDescription
items*ReactNode[]Array of items to display in carousel.
intervalnumber5000Auto-advance interval in milliseconds. 0 disables auto-advance.
showControlsbooleantrueWhether to show next/prev buttons.
showIndicatorsbooleantrueWhether to show dot indicators.
classNamestringAdditional CSS classes for carousel container.

Implementation

Carousel.tsx
tsx
"use client";

import { useState, useEffect, useCallback } from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib";

type CarouselProps = {
  items: React.ReactNode[];
  interval?: number;
  showControls?: boolean;
  showIndicators?: boolean;
  className?: string;
};

export function Carousel({
  items,
  interval = 5000,
  showControls = true,
  showIndicators = true,
  className,
}: CarouselProps) {
  const [currentIndex, setCurrentIndex] = useState(0);
  const [isPaused, setIsPaused] = useState(false);

  const goTo = useCallback((index: number) => {
    setCurrentIndex((index + items.length) % items.length);
  }, [items.length]);

  const next = useCallback(() => {
    goTo(currentIndex + 1);
  }, [currentIndex, goTo]);

  const prev = useCallback(() => {
    goTo(currentIndex - 1);
  }, [currentIndex, goTo]);

  useEffect(() => {
    if (interval <= 0 || isPaused) return;
    const timer = setInterval(next, interval);
    return () => clearInterval(timer);
  }, [next, interval, isPaused]);

  useEffect(() => {
    const handleKey = (e: KeyboardEvent) => {
      if (e.key === "ArrowLeft") prev();
      if (e.key === "ArrowRight") next();
    };
    window.addEventListener("keydown", handleKey);
    return () => window.removeEventListener("keydown", handleKey);
  }, [prev, next]);

  return (
    <div
      className={cn("relative overflow-hidden rounded-xl", className)}
      onMouseEnter={() => setIsPaused(true)}
      onMouseLeave={() => setIsPaused(false)}
      role="region"
      aria-label="Carousel"
    >
      <div
        className="flex transition-transform duration-500 ease-in-out"
        style={{ transform: `translateX(-${currentIndex * 100}%)` }}
      >
        {items.map((item, i) => (
          <div
            key={i}
            className="w-full flex-shrink-0"
            role="group"
            aria-roledescription="slide"
            aria-label={`${i + 1} of ${items.length}`}
          >
            {item}
          </div>
        ))}
      </div>

      {showControls && items.length > 1 && (
        <button
          onClick={prev}
          className="absolute left-2 top-1/2 -translate-y-1/2 p-2 rounded-full bg-white/80 hover:bg-white shadow-lg transition-all"
          aria-label="Previous slide"
        >
          <ChevronLeft className="w-5 h-5 text-[rgb(var(--text-primary))]" />
        </button>
      )}

      {showControls && items.length > 1 && (
        <button
          onClick={next}
          className="absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-full bg-white/80 hover:bg-white shadow-lg transition-all"
          aria-label="Next slide"
        >
          <ChevronRight className="w-5 h-5 text-[rgb(var(--text-primary))]" />
        </button>
      )}

      {showIndicators && items.length > 1 && (
        <div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
          {items.map((_, i) => (
            <button
              key={i}
              onClick={() => goTo(i)}
              className={cn(
                "w-2 h-2 rounded-full transition-all",
                i === currentIndex
                  ? "bg-white w-6"
                  : "bg-white/50 hover:bg-white/70"
              )}
              aria-label={`Go to slide ${i + 1}`}
              aria-current={i === currentIndex ? "true" : "false"}
            />
          ))}
        </div>
      )}
    </div>
  );
}

Accessibility

  • Carousel region must have aria-label describing its purpose.
  • Each slide must have aria-roledescription='slide' and aria-label indicating position.
  • Auto-rotation must pause on hover and focus for accessibility.
  • Controls must have clear aria-labels (e.g., 'Previous slide', 'Next slide').
  • Indicators must indicate current slide with aria-current='true'.
  • Provide a stop/play button for auto-advancing carousels when the timer is less than 5 seconds.