Docs
Stacked Carousel

Stacked Carousel

A modern image carousel with a stacked card effect, smooth transitions, and intuitive navigation.

Installation

Copy and paste the following code into your project.

components/ui/stacked-carousel.tsx

"use client";
 
import React, { useState } from "react";
import { cn } from "@/lib/utils";
 
interface StackedCarouselProps {
  slides: {
    image: string;
  }[];
  className?: string;
}
 
export function StackedCarousel({
  slides,
  className,
}: StackedCarouselProps) {
  const [activeIndex, setActiveIndex] = useState(0);
 
  const nextSlide = () => {
    setActiveIndex((prev) => (prev + 1) % slides.length);
  };
 
  const prevSlide = () => {
    setActiveIndex((prev) => (prev - 1 + slides.length) % slides.length);
  };
 
  const getSlideIndex = (index: number) => {
    const positions = slides.map((_, i) => {
      const diff = (i - activeIndex + slides.length) % slides.length;
      return diff;
    });
    return positions[index];
  };
 
  return (
    <div className={cn("relative h-[400px] w-[400px]", className)}>
      {/* Navigation Buttons */}
      <button
        onClick={prevSlide}
        className="absolute -left-12 top-1/2 z-50 -translate-y-1/2 rounded-full bg-white/10 p-2 text-white backdrop-blur-md transition-all hover:bg-white/20 cursor-pointer"
      >
        <svg
          xmlns="http://www.w3.org/2000/svg"
          fill="none"
          viewBox="0 0 24 24"
          strokeWidth={1.5}
          stroke="currentColor"
          className="h-6 w-6"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            d="M15.75 19.5L8.25 12l7.5-7.5"
          />
        </svg>
      </button>
      <button
        onClick={nextSlide}
        className="absolute -right-12 top-1/2 z-50 -translate-y-1/2 rounded-full bg-white/10 p-2 text-white backdrop-blur-md transition-all hover:bg-white/20 cursor-pointer"
      >
        <svg
          xmlns="http://www.w3.org/2000/svg"
          fill="none"
          viewBox="0 0 24 24"
          strokeWidth={1.5}
          stroke="currentColor"
          className="h-6 w-6"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            d="M8.25 4.5l7.5 7.5-7.5 7.5"
          />
        </svg>
      </button>
 
      {/* Slides */}
      <div className="relative h-full w-full">
        {slides.map((slide, index) => {
          const position = getSlideIndex(index);
          return (
            <div
              key={index}
              className={cn(
                "absolute left-1/2 top-1/2 h-full w-full -translate-x-1/2 -translate-y-1/2 rounded-2xl transition-all duration-700",
                "cursor-pointer hover:scale-105",
                {
                  "z-30": position === 0,
                  "z-20": position === 1 || position === slides.length - 1,
                  "z-10": position === 2 || position === slides.length - 2,
                  "opacity-0": position > 2 && position < slides.length - 2,
                  "-translate-x-[98%]": position === slides.length - 1,
                  "translate-x-[0%]": position === 1,
                  "scale-90": position !== 0,
                  "-rotate-12": position === slides.length - 1,
                  "rotate-12": position === 1,
                },
              )}
              onClick={() => position !== 0 && setActiveIndex(index)}
              style={{
                backgroundImage: `url(${slide.image})`,
                backgroundSize: "cover",
                backgroundPosition: "center",
                boxShadow:
                  position === 0
                    ? "0 0 30px rgba(0, 0, 0, 0.4)"
                    : "0 0 20px rgba(0, 0, 0, 0.3)",
              }}
            >
              <div
                className={cn(
                  "absolute inset-0 rounded-2xl bg-linear-to-br transition-opacity duration-700",
                )}
              />
            </div>
          );
        })}
      </div>
 
      {/* Indicators */}
      <div className="absolute -bottom-12 left-1/2 flex -translate-x-1/2 gap-2">
        {slides.map((_, index) => (
          <button
            key={index}
            onClick={() => setActiveIndex(index)}
            className={cn(
              "h-2 w-2 rounded-full transition-all",
              index === activeIndex
                ? "w-6 bg-white"
                : "bg-white/50 hover:bg-white/75",
            )}
          />
        ))}
      </div>
    </div>
  );
}

Update the import paths to match your project setup.

import StackedCarousel from "@/components/ui/stacked-carousel";

Usage

import StackedCarousel from "@/components/ui/stacked-carousel";
 
const images = [
  {
    image: "https://images.unsplash.com/photo-1604871000636-074fa5117945",
  },
  {
    image: "https://images.unsplash.com/photo-1618172193763-c511deb635ca",
  },
  // ... more images
];
 
export default function Gallery() {
  return <StackedCarousel slides={images} />;
}

Examples

Basic Usage

A simple image carousel with the stacked card effect.

<StackedCarousel slides={images} />

Custom Styling

You can customize the appearance using className.

<StackedCarousel slides={images} className="w-full max-w-3xl mx-auto" />

Props

NameTypeDefaultDescription
slidesImage[][]An array of image objects with image and alt properties.
classNamestring""Additional CSS classes for styling.