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

Cómo crear un carrusel de testimonios en pila de tarjetas 3D en React

Un carrusel de testimonios en pila en React renderiza tres tarjetas simultáneamente con un desplazamiento Y creciente, una scale decreciente y una opacidad decreciente para simular profundidad física. AnimatePresence de Framer Motion y la física de spring impulsan la transición: la tarjeta superior sale en un arco de rotación mientras las tarjetas inferiores suben hacia sus nuevas posiciones.

  • Stack: React 18 + Framer Motion 11 + lucide-react, ~330 líneas, sin dependencias adicionales.
  • API clave: AnimatePresence (modo popLayout), motion.div con la prop layout, config de spring (stiffness 280, damping 32).
  • Auto-rotación cada 3.5s mediante setInterval; los botones prev/next y los indicadores dot permiten el control manual.
  • Accesible: la zona clicable lleva role='button' y aria-label; los botones de navegación tienen aria-labels descriptivos.
  • Funciona en móvil pero el swipe táctil no está implementado, toca la tarjeta o usa los botones.

El Testimonials 3D Stack convierte una lista plana de citas en una baraja física de tarjetas. Siempre hay tres tarjetas visibles a la vez, cada una desplazada hacia abajo y reducida para fingir profundidad; la tarjeta frontal es la única superficie interactiva. Al hacer clic en ella, o al esperar 3,5 segundos, pasa al siguiente testimonio con un arco de salida impulsado por spring, creando la sensación de hojear un paquete real de fichas.

Anatomía

La sección tiene un header centrado (badge, H2, subtítulo) y luego una zona de pila de tarjetas. La pila es un contenedor relative de 560px de ancho y 300px de alto. Dentro, tres elementos motion.div se posicionan en absolute en top:0 y se animan hacia su desplazamiento Y y su scale mediante la tabla STACK_OFFSETS. Las tarjetas se renderizan de atrás hacia adelante (z-index invertido) para que la tarjeta superior se pinte la última. Bajo la pila, los botones chevron prev/next flanquean una tira de dots animados, el dot activo se ensancha a 24px, los inactivos se quedan en 8px.

Cómo funciona

El estado guarda un único entero activeIndex y una dirección (1 o -1). Los tres índices visibles se calculan con `[0, 1, 2].map(offset => (activeIndex + offset) % n)`, así que el deck siempre muestra la tarjeta actual más las dos siguientes. Cada motion.div recibe una key `testimonialIndex-activeIndex`; cuando cambia, AnimatePresence en modo popLayout retira el elemento antiguo e inserta el nuevo. La tarjeta superior entrante (pos === 0) parte de y:-120 con una ligera rotación para que parezca llegar desde arriba. La animación de salida la envía hacia arriba (-160px) con una rotación hacia afuera. Las tarjetas de fondo simplemente suben hacia su nueva posición de profundidad mediante la prop layout, con retrasos escalonados (pos * 0.06s) para que la cascada resulte física.

Cómo crearlo en React

  1. Definir la tabla de profundidad

    Crea un array STACK_OFFSETS con tres entradas, una por capa visible. Cada entrada contiene un desplazamiento Y en píxeles, un factor scale, un valor de opacidad y una cadena box-shadow. La capa 0 está delante, la capa 2 es la más lejana. Estos son los únicos valores que necesitas ajustar para cambiar la sensación de profundidad.

    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. Calcular los tres índices visibles

    A partir de un único valor de estado activeIndex, deriva qué testimonios ocupan las tres posiciones de la pila. Mapea los offsets 0-2 módulo el total. Así nunca necesitas guardar el conjunto visible completo, solo el cursor activo.

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

    Envuelve cada tarjeta en AnimatePresence (mode='popLayout') y dale al motion.div la prop layout para que React reordene las posiciones de profundidad con suavidad. Asigna la key del motion.div sobre testimonialIndex y activeIndex para que una tarjeta que vuelve a entrar tras un ciclo completo dispare de nuevo la animación de entrada. El retraso escalonado de la transición (`pos * 0.06`) hace que cada tarjeta de fondo espere un poco para que la cascada resulte secuencial.

    <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. Conectar el auto-play y los controles

    Un useEffect con setInterval llama a la función advance cada autoPlayInterval milisegundos. La función advance define la dirección y luego incrementa activeIndex módulo n. Los botones prev/next llaman a advance con -1 o 1, y los botones dot saltan directamente calculando el signo de (objetivo - actual) para definir la dirección correctamente antes de actualizar activeIndex.

Cuándo usarlo

Usa este patrón en landing pages donde la prueba social es un motor de conversión principal: secciones de pricing SaaS, portfolios de agencias o cualquier producto que se beneficie de un tratamiento visual premium. La metáfora de profundidad física funciona especialmente bien con identidades de marca de lujo, audaces o elegantes. Evítalo cuando los testimonios son contenido secundario, una cuadrícula más simple o un formato de cita única carga más rápido y compite menos con la información circundante. Evítalo también si tienes menos de tres testimonios; la ilusión de pila se rompe con solo una o dos tarjetas.

Usado por

  • Stripe, Utiliza motivos de tarjetas superpuestas en su marketing para transmitir profundidad y calidad premium en sus páginas de producto.
  • Loom, Rota tarjetas de testimonios apiladas en el hero de su página de inicio para mostrar el impacto en los clientes con interés visual.
  • Framer, Emplea patrones de pila de tarjetas y carruseles impulsados por la física en todo su sitio para demostrar sus propias herramientas de animación.
  • Webflow, Presenta testimonios en tarjetas apiladas con transiciones de profundidad en sus páginas de casos de clientes.

FAQ

¿Por qué el componente renderiza tres tarjetas en vez de una?

La ilusión de pila requiere al menos dos tarjetas visibles detrás de la frontal. Sin ellas, los avances se sienten como una transición slide plana en lugar de retirar una tarjeta de una baraja física. Tres tarjetas dan suficiente profundidad sin el coste en el DOM de renderizar más.

¿Cómo desactivo la auto-rotación?

Pasa autoPlayInterval={0} o un número muy grande, o modifica el useEffect para omitir setInterval cuando una prop como autoPlay valga false. El intervalo se limpia automáticamente al desmontar mediante la función de cleanup.

¿Puedo añadir soporte de swipe para móvil?

Sí. El motion.div de Framer Motion acepta onPanEnd; ahí comprueba los valores offset.x y velocity.x para decidir la dirección, luego llama a advance(1) o advance(-1). El resto de la lógica de animación queda sin cambios.

¿El componente necesita imágenes para los avatares?

No. El subcomponente InitialAvatar genera un círculo de color con la inicial del autor, usando un conjunto fijo de tonos cíclados por índice. Esto mantiene el componente autónomo sin peticiones de red. Cámbialo por una etiqueta img si tienes fotos reales.

"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

Reseñas

Testimonios en pila de tarjetas React (Framer Motion),