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

Créer une pile de cartes témoignages swipeable en React

Une pile de cartes témoignages swipeable en React rend les témoignages comme des cartes empilées en position absolue. La carte de devant réagit au drag horizontal via useMotionValue de Framer Motion et pivote avec useTransform ; un décalage de drag supérieur à 100px déclenche la suppression et révèle la carte suivante.

  • Stack : React 18 + Framer Motion 11 + lucide-react, ~170 lignes, zéro dépendance supplémentaire.
  • API Framer Motion clé : useMotionValue, useTransform, AnimatePresence, drag avec dragElastic.
  • Seule la carte de devant est draggable ; les deux cartes derrière sont rendues à opacité réduite pour simuler la profondeur.
  • Accessible : le contenu des cartes utilise du HTML sémantique ; un bouton de réinitialisation restaure les cartes quand la pile est vide.
  • Fonctionne sur écrans tactiles, le drag de Framer Motion se base sur les pointer events, pas uniquement les événements souris.

Testimonials Stack Swipe apporte un geste mobile familier, le swipe Tinder, aux sections de social proof. Plutôt qu'une grille statique ou un carrousel automatique, les visiteurs rejettent les cartes à gauche ou à droite, ce qui donne à chaque témoignage le sentiment d'une découverte active plutôt que d'un fond sonore. Le pattern fonctionne particulièrement bien sur les produits mobile-first et tout ce qui cible une audience habituée aux apps.

Anatomie

La section comporte un header centré (titre + sous-titre) et un conteneur de cartes à hauteur fixe (480px de large, 320px de haut) qui accueille jusqu'à trois cartes en position absolue. Les cartes sont rendues dans l'ordre inversé de la slice pour que la carte du dessus apparaisse en dernier dans le DOM et se retrouve visuellement au premier plan. L'état de rejet est suivi dans un Set ; quand ce Set atteint la longueur du tableau de témoignages, la vue d'état vide avec bouton de réinitialisation prend le relais.

Comment ça marche

Chaque SwipeCard crée son propre `x` useMotionValue, puis dérive `rotate` (mappé de [-200, 200] à [-15, 15] degrés) et `opacity` (un fondu en cinq points vers les bords) via useTransform. L'axe de drag est verrouillé sur `x` avec une élasticité de 0.9, et dragConstraints ramène la carte au centre si elle est relâchée sous le seuil. Le handler `onDragEnd` vérifie `info.offset.x` ; si la valeur absolue dépasse 100px, il ajoute l'ID de la carte au Set de rejet. AnimatePresence gère l'animation de sortie, un fondu rapide à 0.2s, pour que les cartes ne disparaissent pas brutalement.

Comment le coder en React

  1. Construire l'état de rejet

    Stocke les IDs des cartes rejetées dans un Set à l'intérieur d'un useState. Dérive la pile visible en filtrant le tableau de témoignages par rapport à ce Set. Une fonction de réinitialisation remplace le Set par un nouveau Set vide.

    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. Dériver rotation et opacité depuis la position de drag

    Dans SwipeCard, crée une motion value `x`, puis mappe-la en rotation et opacité avec useTransform. Tout cela tourne sur le thread compositor, aucun re-render React pendant le 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. Rejeter au seuil et animer la sortie

    Passe `drag="x"` avec `dragElastic={0.9}` et `dragConstraints={{ left: 0, right: 0 }}` à la motion.div. Dans `onDragEnd`, lis `info.offset.x` ; appelle `onDismiss()` quand il dépasse 100px dans n'importe quelle direction. Enveloppe la liste de cartes dans AnimatePresence avec un fondu de sortie.

    onDragEnd={(_, info) => {
      if (Math.abs(info.offset.x) > 100) onDismiss();
    }}
  4. Afficher au plus trois cartes en profondeur

    Prends une slice de trois éléments du tableau restant, inverse-la, puis rends chaque carte. Le dernier élément après inversion est la carte de devant ; donne-lui `isFront={true}` pour qu'elle reçoive le drag et les motion values. Les autres restent derrière à 0.7 d'opacité comme repères de profondeur statiques.

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

Quand l'utiliser

Utilise ce pattern quand tu veux que le social proof soit interactif plutôt que décoratif, applications grand public, marketplaces, SaaS mobile-first, ou toute landing page ciblant une audience habituée aux gestes de swipe. Évite-le sur les pages B2B enterprise où les acheteurs s'attendent à voir logos et citations d'un coup d'oeil, ou quand tu as moins de quatre témoignages (l'effet de pile disparaît avec deux cartes). Prévois un moyen de voir tous les témoignages d'un coup pour les utilisateurs qui ne swiperont pas.

Utilisé par

  • Tinder, L'inventeur du mécanisme de rejet de carte par swipe, devenu un pattern d'interaction largement compris.
  • Bumble, Utilise le même mécanisme de pile swipeable pour les profils, renforçant à quel point le drag horizontal est naturel sur tactile.
  • Product Hunt, Applique une UI de pile de cartes dans son flux de vote Golden Kitty, permettant aux utilisateurs de swiper les candidats.

FAQ

Le swipe fonctionne-t-il sur mobile et écrans tactiles ?

Oui. Le système de drag de Framer Motion écoute les pointer events, qui couvrent souris et tactile. Le seuil de 100px de décalage correspond naturellement à la distance d'un swipe au doigt.

Pourquoi la pile n'affiche-t-elle que trois cartes à la fois ?

Rendre plus de trois cartes empilerait des noeuds DOM inutilement, et l'illusion de profondeur s'effondre de toute façon après la troisième. Limiter à trois garde le composant léger tout en signalant 'il y en a d'autres derrière'.

Comment ajouter un indicateur visuel (flèche ou label) montrant dans quel sens swiper ?

Mappe la motion value `x` sur une opacité pour un label gauche et un label droit via useTransform. Affiche 'Oui' à droite quand x > 20 et 'Passer' à gauche quand x < -20, comme des overlays en position absolue sur la carte.

Puis-je déclencher le rejet programmatiquement, sans drag ?

Oui. Expose la fonction `dismiss` à des boutons parents (comme des contrôles flèches gauche/droite). Pour que l'animation de sortie se déclenche, assure-toi que la carte est encore dans AnimatePresence quand l'ID est ajouté au Set de rejet.

"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

Avis

Pile de cartes avis swipeable en React, Framer Motion