Retour au catalogue

Testimonials Stack Swipe

Pile de cartes temoignages a swiper style Tinder avec physique de ressort. Drag interactif gauche/droite.

testimonialscomplex Both Responsive a11y
playfulboldsaasagencyuniversalcentered
Theme

Cómo crear una pila de tarjetas de testimonios swipeable en React

Una pila de tarjetas de testimonios swipeable en React renderiza los testimonios como tarjetas apiladas en posición absoluta. La tarjeta frontal responde al drag horizontal mediante useMotionValue de Framer Motion y rota con useTransform; un desplazamiento de drag superior a 100px activa el descarte y revela la tarjeta que hay debajo.

  • Stack: React 18 + Framer Motion 11 + lucide-react, ~170 líneas, cero dependencias adicionales.
  • API principal de Framer Motion: useMotionValue, useTransform, AnimatePresence, drag con dragElastic.
  • Solo la tarjeta frontal es arrastrable; las dos tarjetas de detrás se renderizan con opacidad reducida para simular profundidad.
  • Accesible: el contenido de las tarjetas usa markup semántico; un botón de reinicio restaura las tarjetas descartadas cuando la pila se vacía.
  • Funciona en pantallas táctiles, el drag de Framer Motion se dispara con pointer events, no solo con eventos de ratón.

Testimonials Stack Swipe lleva un gesto móvil familiar, el swipe de Tinder, a las secciones de social proof. En lugar de una cuadrícula estática o un carrusel en bucle, los visitantes descartan activamente las tarjetas a izquierda o derecha, lo que hace que cada testimonio se sienta como un descubrimiento deliberado en vez de ruido de fondo. El patrón funciona especialmente bien en productos mobile-first y en cualquier cosa dirigida a un público joven y acostumbrado a las apps.

Anatomía

La sección tiene un header centrado (título + subtítulo) y un contenedor de tarjetas de altura fija (480px de ancho, 320px de alto) que alberga hasta tres tarjetas a la vez mediante posicionamiento absoluto. Las tarjetas se renderizan en orden inverso de la slice para que la tarjeta superior aparezca la última en el DOM y quede visualmente al frente. El estado de descarte se guarda en un Set; cuando ese Set alcanza la longitud del array de testimonios, la vista de estado vacío con botón de reinicio toma el relevo.

Cómo funciona

Cada SwipeCard crea su propio useMotionValue `x`, luego deriva `rotate` (mapeado de [-200, 200] a [-15, 15] grados) y `opacity` (un fundido de cinco puntos hacia los bordes) mediante useTransform. El eje de drag se bloquea en `x` con una elasticidad de 0.9, y dragConstraints devuelve la tarjeta al centro cuando se suelta por debajo del umbral. El handler `onDragEnd` comprueba `info.offset.x`; si el valor absoluto supera los 100px, añade el ID de la tarjeta al Set de descartadas. AnimatePresence gestiona la animación de salida, un fundido rápido de 0.2s, para que las tarjetas no desaparezcan de golpe.

Cómo crearlo en React

  1. Construir el estado de descarte

    Guarda los IDs de las tarjetas descartadas en un Set dentro de useState. Deriva la pila visible filtrando el array de testimonios contra ese Set. Una función de reinicio reemplaza el Set por uno nuevo vacío.

    const [gone, setGone] = useState(new Set<string>());
    const remaining = testimonials.filter((t) => !gone.has(t.id));
    const dismiss = (id: string) => setGone((prev) => new Set(prev).add(id));
    const reset = () => setGone(new Set());
  2. Derivar rotación y opacidad desde la posición de drag

    Dentro de SwipeCard, crea una motion value `x`, luego mapéala a rotación y opacidad con useTransform. Todo esto se ejecuta en el thread del compositor, sin re-renders de React durante el drag.

    const x = useMotionValue(0);
    const rotate = useTransform(x, [-200, 200], [-15, 15]);
    const opacity = useTransform(x, [-200, -100, 0, 100, 200], [0.5, 1, 1, 1, 0.5]);
  3. Descartar al superar el umbral y animar la salida

    Pasa `drag="x"` con `dragElastic={0.9}` y `dragConstraints={{ left: 0, right: 0 }}` al motion.div. En `onDragEnd`, lee `info.offset.x`; llama a `onDismiss()` cuando supere los 100px en cualquier dirección. Envuelve la lista de tarjetas en AnimatePresence con un fundido de salida.

    onDragEnd={(_, info) => {
      if (Math.abs(info.offset.x) > 100) onDismiss();
    }}
  4. Renderizar como máximo tres tarjetas en profundidad

    Toma una slice de tres elementos del array restante, inviértela, luego renderiza cada una. El último elemento tras invertir es la tarjeta frontal; asígnale `isFront={true}` para que reciba el drag y las motion values en vivo. Las demás quedan detrás con 0.7 de opacidad como indicadores de profundidad estáticos.

    {remaining.slice(0, 3).reverse().map((t, i, arr) => (
      <SwipeCard
        key={t.id}
        testimonial={t}
        onDismiss={() => dismiss(t.id)}
        isFront={i === arr.length - 1}
      />
    ))}

Cuándo usarlo

Recurre a este patrón cuando quieras que el social proof se sienta interactivo en vez de decorativo, apps de consumo, productos de marketplace, SaaS mobile-first, o cualquier landing page dirigida a un público cómodo con los gestos de swipe. Evítalo en páginas B2B enterprise donde los compradores esperan ver logos y citas densas de un vistazo, o cuando tienes menos de cuatro testimonios (el efecto de pila desaparece con dos tarjetas). Ofrece una forma de ver todos los testimonios a la vez para los usuarios que no se molesten en swipear.

Usado por

  • Tinder, El creador del mecanismo de descarte de tarjetas por swipe, hoy un patrón de interacción ampliamente comprendido.
  • Bumble, Usa el mismo mecanismo de pila swipeable para las tarjetas de perfil, reforzando lo natural que resulta el drag horizontal en pantallas táctiles.
  • Product Hunt, Aplica una UI de pila de tarjetas en su flujo de votación Golden Kitty, permitiendo a los usuarios swipear los nominados.

FAQ

¿Funciona el swipe en móvil y pantallas táctiles?

Sí. El sistema de drag de Framer Motion escucha los pointer events, que cubren tanto ratón como táctil. El umbral de desplazamiento de 100px se traduce de forma natural a la distancia de un swipe con el dedo.

¿Por qué la pila muestra solo tres tarjetas a la vez?

Renderizar más de tres apilaría nodos DOM innecesariamente y la ilusión de profundidad se rompe de todas formas más allá de la tercera tarjeta. Limitar a tres mantiene el componente ligero sin dejar de transmitir 'hay más detrás de esta'.

¿Cómo añado una pista visual (flecha o etiqueta) que muestre hacia qué lado swipear?

Mapea la motion value `x` a una opacidad para una etiqueta izquierda y una etiqueta derecha mediante useTransform. Muestra 'Me gusta' a la derecha cuando x > 20 y 'Saltar' a la izquierda cuando x < -20, ambas como overlays en posición absoluta sobre la tarjeta.

¿Puedo activar el descarte programáticamente, sin arrastrar?

Sí. Expón la función `dismiss` a botones del componente padre (como controles de flecha izquierda/derecha). Para que se dispare la animación de salida, asegúrate de que la tarjeta siga dentro de AnimatePresence cuando el ID se añade al Set de descartadas.

"use client";

import { useState } from "react";
import { motion, useMotionValue, useTransform, AnimatePresence } from "framer-motion";
import { Star } from "lucide-react";

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

interface TestimonialsStackSwipeProps {
  title?: string;
  subtitle?: string;
  testimonials?: Testimonial[];
}

const EASE = [0.16, 1, 0.3, 1] as const;

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Reseñas

Pila de tarjetas de opiniones swipeable en React, Framer