Retour au catalogue

Testimonials 3D Stack

Cards testimonials empilées comme un jeu de cartes physiques avec profondeur (Y offset + scale + ombre). La card du dessus sort en arc avec AnimatePresence, les suivantes montent. Auto-rotate toutes les 3.5s, navigation manuelle prev/next.

testimonialscomplex Both Responsive a11y
elegantboldluxurysaasagencyuniversalstackedcentered
Theme

How to build a 3D stacked card testimonials carousel in React

A stacked-card testimonials carousel in React renders three cards simultaneously with increasing Y offset, decreasing scale, and decreasing opacity to simulate physical depth. Framer Motion's AnimatePresence and spring physics drive the transition: the top card exits with a rotation arc while the cards below slide up into new positions.

  • Stack: React 18 + Framer Motion 11 + lucide-react, ~330 lines, no extra dependencies.
  • Core API: AnimatePresence (popLayout mode), motion.div with layout prop, spring config (stiffness 280, damping 32).
  • Auto-rotates every 3.5s via setInterval; prev/next buttons and dot indicators allow manual control.
  • Accessible: the click target carries role='button' and aria-label; nav buttons have descriptive aria-labels.
  • Works on mobile but touch-swipe is not implemented, tap the card or use the buttons.

The Testimonials 3D Stack turns a flat list of quotes into a physical card deck. Three cards are always visible at once, each shifted down and scaled back to fake depth; the frontmost card is the only interactive surface. Clicking it, or waiting 3.5 seconds, flips to the next testimonial with a spring-driven exit arc, creating the feeling of thumbing through a real stack of notes.

Anatomy

The section has a centered header (badge, H2, subtitle) then a stacked card area. The stack is a 560px-wide, 300px-tall relative container. Inside it, three motion.div elements are positioned absolutely at top:0 and animated to their Y offset and scale via the STACK_OFFSETS table. Cards are rendered back-to-front (reversed z-index) so the top card paints last. Below the stack, prev/next chevron buttons flank an animated dot strip, active dot widens to 24px, inactive dots stay at 8px.

How it works

State tracks a single activeIndex integer and a direction (1 or -1). The three visible indices are computed as `[0, 1, 2].map(offset => (activeIndex + offset) % n)`, so the deck always shows the current card plus the next two. Each motion.div receives a key of `testimonialIndex-activeIndex`; when it changes, AnimatePresence in popLayout mode removes the old element and inserts the new one. The entering top card (pos === 0) starts at y:-120 with a slight rotation so it appears to fly in from above. The exit animation sends it upward (-160px) with an outward rotation. Background cards just slide up to their new depth position via the layout prop, with staggered delays (pos * 0.06s) so the cascade reads as physical.

How to build it in React

  1. Define the depth table

    Create a STACK_OFFSETS array with three entries, one per visible layer. Each entry holds a Y pixel offset, a scale factor, an opacity value, and a box-shadow string. Layer 0 is front, layer 2 is furthest back. These are the only values you need to tune to change the depth feel.

    const STACK_OFFSETS = [
      { y: 0,  scale: 1,    opacity: 1,    shadow: "0 24px 80px ..." },
      { y: 14, scale: 0.96, opacity: 0.85, shadow: "0 12px 40px ..." },
      { y: 26, scale: 0.92, opacity: 0.6,  shadow: "0 6px 20px ..."  },
    ];
  2. Compute the three visible indices

    From a single activeIndex state value, derive which testimonials fill the three stack positions. Map offsets 0-2 modulo the total count. This means you never need to store the full visible set, just the active cursor.

    const stackIndices = [0, 1, 2].map(
      (offset) => (activeIndex + offset) % n
    );
  3. Animate with AnimatePresence + layout

    Wrap each card in AnimatePresence (mode='popLayout') and give the motion.div the layout prop so React re-orders depth positions smoothly. Key the motion.div on both testimonialIndex and activeIndex so a card re-entering after a full loop triggers the entrance animation again. The staggered transition delay (`pos * 0.06`) makes each background card wait slightly so the cascade feels sequential.

    <AnimatePresence key={testimonialIndex} mode="popLayout">
      <motion.div
        key={`${testimonialIndex}-${activeIndex}`}
        layout
        initial={pos === 0 ? { y: -120, rotate: -6, scale: 0.9 } : false}
        animate={{ y: offset.y, scale: offset.scale, opacity: offset.opacity }}
        exit={{ y: -160, rotate: -8, scale: 0.88 }}
        transition={{ ...SPRING, delay: pos * 0.06 }}
      />
    </AnimatePresence>
  4. Wire auto-play and controls

    A useEffect with setInterval calls the advance function every autoPlayInterval milliseconds. The advance function sets direction then increments activeIndex modulo n. Both prev/next buttons call advance with -1 or 1, and dot buttons jump directly by computing the sign of (target - current) to set direction correctly before updating activeIndex.

When to use it

Use this pattern on landing pages where social proof is a primary conversion driver: SaaS pricing sections, agency portfolio pages, or any product that benefits from premium visual treatment. The physical depth metaphor works especially well with luxury, bold, or elegant brand identities. Skip it when testimonials are secondary content, a simpler grid or single-quote layout loads faster and competes less with surrounding information. Also skip it if you have fewer than three testimonials; the stack illusion breaks with only one or two cards.

Used by

  • Stripe, Uses layered card motifs in marketing to convey depth and premium quality across its product pages.
  • Loom, Rotates stacked testimonial cards in its homepage hero to showcase customer impact with visual interest.
  • Framer, Employs card-stack and physics-driven carousel patterns throughout its site to demonstrate its own animation tooling.
  • Webflow, Features stacked card testimonials with depth transitions on its customer showcase pages.

FAQ

Why does the component render three cards instead of one?

The stacking illusion requires at least two cards visible behind the front one. Without them, advances feel like a flat slide transition rather than pulling a card off a physical deck. Three cards give enough depth without the DOM overhead of rendering more.

How do I disable auto-rotate?

Pass autoPlayInterval={0} or a very large number, or modify the useEffect to skip setInterval when a prop like autoPlay is false. The interval is cleared on unmount automatically via the cleanup function.

Can I add swipe support for mobile?

Yes. Framer Motion's motion.div accepts onPanEnd; check the offset.x and velocity.x values there to decide direction, then call advance(1) or advance(-1). The rest of the animation logic stays unchanged.

Does the component need images for the avatars?

No. The InitialAvatar sub-component generates a colored circle with the author's first initial, using a fixed set of hues cycled by index. This keeps the component self-contained with no network requests. Swap it for an img tag if you have real photos.

"use client";

import { useState, useEffect, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronLeft, ChevronRight } from "lucide-react";

interface Testimonial {
  id: string;
  content: string;
  authorName: string;
  authorRole: string;
  company?: string;
  rating?: number;
}

interface Testimonials3dStackProps {
  title?: string;
  subtitle?: string;
  badge?: string;
  testimonials?: Testimonial[];
  autoPlayInterval?: number;
}

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Reviews

React Stacked Cards Testimonials (Framer Motion), Tutorial