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
Interaction states
| State | Appearance | Notes |
|---|---|---|
| Idle | Auto-advances after interval | Default state; timer running |
| Hover | Auto-advance paused | Allows reading content without rushing |
| Manual navigation | Slide transitions with animation | User clicks prev/next or indicators |
| Keyboard active | Arrow keys navigate slides | Focus 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
| Prop | Type | Default | Description |
|---|---|---|---|
items* | ReactNode[] | — | Array of items to display in carousel. |
interval | number | 5000 | Auto-advance interval in milliseconds. 0 disables auto-advance. |
showControls | boolean | true | Whether to show next/prev buttons. |
showIndicators | boolean | true | Whether to show dot indicators. |
className | string | — | Additional 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.