Créer un carousel de témoignages en pile 3D avec React
Un carousel de témoignages en pile rend trois cartes simultanément avec un décalage Y croissant, une scale décroissante et une opacité décroissante pour simuler la profondeur physique. AnimatePresence de Framer Motion et les springs pilotent la transition : la carte du dessus sort en arc de rotation pendant que les cartes du dessous remontent vers leurs nouvelles positions.
- Stack : React 18 + Framer Motion 11 + lucide-react, ~330 lignes, zéro dépendance supplémentaire.
- API clé : AnimatePresence (mode popLayout), motion.div avec la prop layout, spring (stiffness 280, damping 32).
- Auto-rotation toutes les 3.5s via setInterval ; boutons prev/next et indicateurs dot permettent la navigation manuelle.
- Accessible : la zone cliquable porte role='button' et aria-label ; les boutons nav ont des aria-labels descriptifs.
- Fonctionne sur mobile mais le swipe tactile n'est pas implémenté, on tape sur la carte ou utilise les boutons.
Les Testimonials 3D Stack transforment une liste de citations en jeu de cartes physique. Trois cartes sont toujours visibles simultanément, chacune décalée vers le bas et réduite pour simuler la profondeur ; la carte de devant est la seule surface interactive. Cliquer dessus, ou attendre 3,5 secondes, passe au témoignage suivant avec un arc de sortie en spring, comme si l'on feuilletait un vrai paquet de fiches.
Anatomie
La section comprend un header centré (badge, H2, sous-titre) puis une zone de pile de cartes. La pile est un conteneur relative de 560px de large et 300px de haut. À l'intérieur, trois motion.div sont positionnés en absolu à top:0 et animés vers leur décalage Y et scale via le tableau STACK_OFFSETS. Les cartes sont rendues du bas vers le haut (z-index inversé) pour que la carte du dessus soit peinte en dernier. Sous la pile, des boutons chevron prev/next encadrent une bande de dots animés, le dot actif s'élargit à 24px, les inactifs restent à 8px.
Comment ça marche
L'état gère un seul entier activeIndex et une direction (1 ou -1). Les trois indices visibles sont calculés via `[0, 1, 2].map(offset => (activeIndex + offset) % n)`, donc le deck affiche toujours la carte courante et les deux suivantes. Chaque motion.div reçoit une clé `testimonialIndex-activeIndex` ; quand elle change, AnimatePresence en mode popLayout retire l'ancien élément et insère le nouveau. La carte entrante (pos === 0) part de y:-120 avec une légère rotation pour donner l'impression d'arriver par en haut. L'animation de sortie l'envoie vers le haut (-160px) avec une rotation sortante. Les cartes d'arrière-plan remontent simplement vers leur nouvelle position de profondeur via la prop layout, avec des délais échelonnés (pos * 0.06s) pour que la cascade paraisse physique.
Comment le coder en React
Définir le tableau de profondeur
Crée un tableau STACK_OFFSETS avec trois entrées, une par couche visible. Chaque entrée contient un décalage Y en pixels, un facteur scale, une valeur d'opacité et une chaîne box-shadow. La couche 0 est devant, la couche 2 est la plus éloignée. Ce sont les seules valeurs à ajuster pour modifier le rendu de profondeur.
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 ..." }, ];Calculer les trois indices visibles
À partir d'un seul état activeIndex, dérive quels témoignages occupent les trois positions de la pile. Mappe les offsets 0-2 modulo le total. Tu n'as jamais besoin de stocker l'ensemble visible, juste le curseur actif.
const stackIndices = [0, 1, 2].map( (offset) => (activeIndex + offset) % n );Animer avec AnimatePresence + layout
Enveloppe chaque carte dans AnimatePresence (mode='popLayout') et donne la prop layout au motion.div pour que React réordonne les positions de profondeur en douceur. Key le motion.div sur testimonialIndex et activeIndex pour qu'une carte qui revient après un tour complet rejoue l'animation d'entrée. Le délai échelonné (`pos * 0.06`) fait attendre légèrement chaque carte d'arrière-plan pour que la cascade paraisse séquentielle.
<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>Brancher l'auto-play et les contrôles
Un useEffect avec setInterval appelle la fonction advance toutes les autoPlayInterval millisecondes. La fonction advance définit la direction puis incrémente activeIndex modulo n. Les boutons prev/next appellent advance avec -1 ou 1, et les boutons dot sautent directement en calculant le signe de (cible - courant) pour définir la direction correctement avant de mettre à jour activeIndex.
Quand l'utiliser
À utiliser sur les landing pages où la preuve sociale est un levier de conversion principal : sections pricing SaaS, portfolios d'agences, ou tout produit qui bénéficie d'un traitement visuel premium. La métaphore de profondeur physique fonctionne particulièrement bien avec les identités de marque luxe, audacieuses ou élégantes. À éviter si les témoignages sont du contenu secondaire, une grille simple ou un format citation unique charge plus vite et entre moins en concurrence avec les informations voisines. À éviter aussi si tu as moins de trois témoignages ; l'illusion de pile ne tient pas avec une ou deux cartes.
Utilisé par
- Stripe, Utilise des motifs de cartes superposées dans son marketing pour transmettre la profondeur et la qualité premium sur ses pages produit.
- Loom, Fait défiler des cartes de témoignages empilées dans son hero pour mettre en valeur l'impact client avec un intérêt visuel.
- Framer, Utilise des piles de cartes et des carousels pilotés par la physique sur tout son site pour démontrer ses propres outils d'animation.
- Webflow, Présente des témoignages en cartes empilées avec transitions de profondeur sur ses pages de mise en avant clients.
FAQ
Pourquoi le composant rend-il trois cartes au lieu d'une ?
L'illusion de pile nécessite au moins deux cartes visibles derrière celle de devant. Sans elles, les transitions ressemblent à un simple slide plat plutôt qu'à un retrait de carte d'un paquet physique. Trois cartes suffisent à créer la profondeur sans le coût DOM d'en rendre davantage.
Comment désactiver l'auto-rotation ?
Passe autoPlayInterval={0} ou un très grand nombre, ou modifie le useEffect pour ne pas lancer setInterval quand une prop comme autoPlay vaut false. L'intervalle est nettoyé au démontage automatiquement via la fonction de cleanup.
Peut-on ajouter le swipe sur mobile ?
Oui. Le motion.div de Framer Motion accepte onPanEnd ; vérifie les valeurs offset.x et velocity.x pour décider de la direction, puis appelle advance(1) ou advance(-1). Le reste de la logique d'animation reste inchangé.
Le composant a-t-il besoin d'images pour les avatars ?
Non. Le sous-composant InitialAvatar génère un cercle coloré avec l'initiale de l'auteur, en utilisant un ensemble fixe de teintes cyclées par index. Cela rend le composant autonome sans requêtes réseau. Remplace-le par une balise img si tu as de vraies photos.