# Carousel Stacked Fade — React Carousel Component

> Slides sit in a receding stack; the front one drops away and fades while the next rises forward, on a timer that pauses on hover.

- Category: [Carousels](https://pixelpapa.com/components/carousels.md)
- Tags: carousel, stack, fade, autoplay, testimonial
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/carousels/carousel-stacked-fade

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/carousel-stacked-fade.json"
```

The file is written to `components/pixelpapa/carousel-stacked-fade.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { ChevronLeft, ChevronRight, Quote } from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import { Children, useEffect, useState, type FocusEvent, type KeyboardEvent, type ReactNode } from "react";
import { cn } from "@/lib/utils";

type StackedCarouselProps = {
  children: ReactNode;
  /** Milliseconds per slide; 0 turns autoplay off. */
  interval?: number;
  label?: string;
  /** Set the height here; slides fill it. */
  className?: string;
};

export function StackedCarousel({ children, interval = 4200, label = "Carousel", className }: StackedCarouselProps) {
  const slides = Children.toArray(children);
  const n = slides.length;
  const [index, setIndex] = useState(0);
  const [paused, setPaused] = useState(false);
  const reduce = useReducedMotion();
  const go = (d: number) => setIndex((i) => (i + d + n) % n);

  useEffect(() => {
    if (!interval || paused || reduce) return;
    const t = setTimeout(() => setIndex((i) => (i + 1) % n), interval);
    return () => clearTimeout(t);
  }, [index, interval, paused, reduce, n]);

  function onKeyDown(e: KeyboardEvent) {
    if (e.key === "ArrowRight") go(1);
    else if (e.key === "ArrowLeft") go(-1);
  }

  function onBlur(e: FocusEvent<HTMLDivElement>) {
    if (!e.currentTarget.contains(e.relatedTarget)) setPaused(false);
  }

  const arrow = "flex size-9 items-center justify-center rounded-full border border-border bg-card text-foreground outline-none transition hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring";
  return (
    <div
      role="region"
      aria-roledescription="carousel"
      aria-label={label}
      onPointerEnter={() => setPaused(true)}
      onPointerLeave={() => setPaused(false)}
      onFocus={() => setPaused(true)}
      onBlur={onBlur}
      onKeyDown={onKeyDown}
      className="w-full"
    >
      <div className={cn("relative mt-8", className)}>
        {slides.map((slide, i) => {
          const offset = (i - index + n) % n;
          // The slide that just left the front waits below, invisible, before rejoining the back.
          const leaving = offset === n - 1 && n > 3;
          const depth = Math.min(offset, 3);
          return (
            <motion.div
              key={i}
              aria-hidden={offset !== 0}
              className={cn("absolute inset-0 origin-top", offset !== 0 && "pointer-events-none")}
              style={{ zIndex: n - offset }}
              initial={false}
              animate={
                leaving
                  ? { y: 44, scale: 1.03, opacity: 0, filter: "blur(6px)" }
                  : { y: depth * -15, scale: 1 - depth * 0.06, opacity: offset > 2 ? 0 : 1 - depth * 0.3, filter: "blur(0px)" }
              }
              transition={{ type: "spring", stiffness: 230, damping: 28 }}
            >
              {slide}
            </motion.div>
          );
        })}
      </div>

      <div className="mt-5 flex items-center justify-between">
        <div className="flex items-center gap-3">
          <span className="text-sm text-muted-foreground tabular-nums">
            <span className="font-medium text-foreground">{String(index + 1).padStart(2, "0")}</span> / {String(n).padStart(2, "0")}
          </span>
          <span className="relative h-0.5 w-24 overflow-hidden rounded-full bg-border">
            {/* Restarts with each slide; freezes while paused. */}
            <span
              key={index}
              className="absolute inset-0 origin-left bg-primary"
              style={{ animation: interval && !reduce ? `pp-carousel-stacked-fade-fill ${interval}ms linear forwards` : undefined, animationPlayState: paused ? "paused" : "running" }}
            />
          </span>
          <style>{`@keyframes pp-carousel-stacked-fade-fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }`}</style>
        </div>
        <div className="flex gap-2">
          <button type="button" aria-label="Previous slide" onClick={() => go(-1)} className={arrow}>
            <ChevronLeft className="size-4" />
          </button>
          <button type="button" aria-label="Next slide" onClick={() => go(1)} className={arrow}>
            <ChevronRight className="size-4" />
          </button>
        </div>
      </div>
    </div>
  );
}

const quotes = [
  { text: "We cut our release checklist from forty steps to six. The team stopped dreading Thursdays.", name: "Hana Okonkwo", role: "Engineering lead, Tidewater", initials: "HO", hue: 262 },
  { text: "The first tool our designers and our finance team both asked to keep after the trial ended.", name: "Luca Ferrand", role: "COO, Atelier Nord", initials: "LF", hue: 25 },
  { text: "Onboarding took an afternoon. Two weeks later we had retired three internal dashboards.", name: "Sofia Almeida", role: "Head of Data, Parcel & Co", initials: "SA", hue: 160 },
  { text: "Support answered in four minutes on a Sunday. I did not know that was still possible.", name: "Dev Raman", role: "Founder, Quillbase", initials: "DR", hue: 310 },
];

export default function CarouselStackedFadeDemo() {
  return (
    <div className="w-full max-w-lg">
      <StackedCarousel label="Customer stories" className="h-[236px]">
        {quotes.map((q) => (
          <figure key={q.name} className="flex size-full flex-col justify-between rounded-3xl border border-border bg-card p-7 shadow-xl shadow-primary/5">
            <div>
              <Quote className="size-6 fill-primary/15 text-primary" />
              <blockquote className="mt-3 font-serif text-[22px] leading-snug text-pretty text-card-foreground">{q.text}</blockquote>
            </div>
            <figcaption className="flex items-center gap-3">
              <span
                className="flex size-10 items-center justify-center rounded-full text-sm font-semibold text-white"
                style={{ background: `linear-gradient(135deg, oklch(0.75 0.15 ${q.hue}), oklch(0.5 0.2 ${q.hue + 40}))` }}
              >
                {q.initials}
              </span>
              <span className="text-sm">
                <span className="block font-medium text-card-foreground">{q.name}</span>
                <span className="block text-muted-foreground">{q.role}</span>
              </span>
            </figcaption>
          </figure>
        ))}
      </StackedCarousel>
    </div>
  );
}
```

## AI prompt

```text
Add the "Carousel Stacked Fade" component from PixelPapa UI to this project.

What it is: Slides sit in a receding stack; the front one drops away and fades while the next rises forward, on a timer that pauses on hover.
Category: Carousels. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/carousel-stacked-fade.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/carousel-stacked-fade.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/carousel-stacked-fade" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More carousels

- [Carousel Coverflow 3D](https://pixelpapa.com/components/carousels/carousel-coverflow-3d.md) (Pro): Covers fan out in perspective around a centred item; swipe, click a side cover or use the arrow keys and they swing through on springs.
- [Carousel Marquee Infinite](https://pixelpapa.com/components/carousels/carousel-marquee-infinite.md): An endless row that glides in either direction and eases to a stop under the pointer instead of freezing, then eases back up to speed.
- [Carousel Progress Autoplay](https://pixelpapa.com/components/carousels/carousel-progress-autoplay.md) (Pro): Story-style slides with segmented progress bars that fill in real time; tap either side to move, press and hold to pause.
- [Carousel Snap Drag](https://pixelpapa.com/components/carousels/carousel-snap-drag.md) (Pro): A draggable track that carries your throw's momentum to the nearest slide, with arrows, stretching dots and arrow-key control.
- [Carousel Thumbnail Gallery](https://pixelpapa.com/components/carousels/carousel-thumbnail-gallery.md) (Pro): A main stage with a thumbnail strip; slides push in from the direction you travel and a ring glides between thumbnails.
- [Carousel Vertical Ticker](https://pixelpapa.com/components/carousels/carousel-vertical-ticker.md): A live feed where each new row springs in at the top, the rest step down a slot and the oldest fades off the bottom, pausing on hover.
