Retour au catalogue

Blog Card Stack

Articles empiles avec rotation alternee. Au hover, les cards se deplient en eventail avec animation fluide.

blogcomplex Both Responsive a11y
playfulboldagencysaasuniversalcentered
Theme

Créer un effet de cartes empilées en eventail React avec Framer Motion

Un jeu de cartes en eventail React place jusqu'à 5 cartes en absolu dans un conteneur de hauteur fixe, chacune décalée d'une petite rotation calculée depuis son index. Au survol, Framer Motion anime chaque carte vers une rotation plus large et une translation X, les déployant en eventail. Tout l'effet repose sur un seul prop animate piloté par un booléen isExpanded.

  • Stack : React + Framer Motion 11 + lucide-react, ~137 lignes, zéro dépendance supplémentaire.
  • API clé : useState, motion.a, AnimatePresence, prop animate avec rotate/x/scale.
  • Chaque carte est une balise ancre, donc la section est navigable au clavier et compatible lecteurs d'écran.
  • Caveat mobile : le déploiement repose sur mouseenter/mouseleave, qui ne se déclenchent pas sur écran tactile.
  • Supporte 3 à 5 articles ; le calcul du point médian centre la rotation de façon symétrique.

Blog Card Stack est une section blog React qui présente les articles comme un jeu de cartes physique. Les cartes reposent empilées avec de légères rotations alternées, et un simple survol les déploie en eventail que le lecteur peut parcourir. Il transforme une liste d'articles en un moment tactile et curieux, sans aucun scroll.

Anatomie

Un en-tête de section centré (label eyebrow + titre serif italique) surplombe un conteneur de 420px de haut. A l'intérieur, jusqu'à 5 cartes motion.a sont positionnées en absolu, toutes partageant le même inset:0 pour occuper le même espace. Chaque carte comporte une image de fond à faible opacité, une surface de carte, un badge de catégorie, le titre de l'article avec une icône ArrowUpRight, un extrait et une date. Les cartes s'empilent visuellement via z-index et une ombre portée par carte avec un flou croissant.

Comment ça marche

La logique d'empilement et de déploiement vit entièrement dans le prop animate de chaque motion.a. Pour une liste de N cartes, le point médian est (N-1)/2. A l'état fermé, chaque carte tourne de (i - mid) * 3 degrés et reste à x:0. A l'état ouvert, la rotation s'élargit à (i - mid) * 8 degrés et x saute à (i - mid) * 120px. L'échelle diminue légèrement avec la profondeur (1 - i * 0.02) à l'état fermé et se fixe à 0.88 pour toutes à l'état ouvert. Un tableau d'easing custom [0.16, 1, 0.3, 1] donne à la transition un effet vif avec une arrivée douce. Tout le basculement d'état se produit sur onMouseEnter/onMouseLeave du conteneur parent.

Comment le coder en React

  1. Mettre en place le conteneur et l'état d'expansion

    Crée un conteneur relative avec une hauteur fixe (420px convient pour des cartes portrait) et un maxWidth centré avec margin auto. Attache onMouseEnter et onMouseLeave pour basculer un seul booléen isExpanded. Positionne toutes les cartes enfants en absolu pour qu'elles occupent le même empreinte.

    const [isExpanded, setIsExpanded] = useState(false);
    
    <div
      onMouseEnter={() => setIsExpanded(true)}
      onMouseLeave={() => setIsExpanded(false)}
      style={{ position: "relative", height: "420px", maxWidth: "640px", margin: "0 auto" }}
    >
  2. Calculer la rotation et la translation de chaque carte

    Pour chaque carte à l'index i, calcule la valeur mid comme (items.length - 1) / 2. La rotation d'empilement est (i - mid) * 3 degrés ; la rotation d'eventail est (i - mid) * 8 degrés. Le décalage X de l'eventail est (i - mid) * 120px, soit environ 120px entre les centres des cartes une fois déployées. Ces formules centrent le déploiement symétriquement quel que soit le nombre de cartes.

    const mid = (items.length - 1) / 2;
    const stackRotate = (i - mid) * 3;
    const fanRotate = (i - mid) * 8;
    const fanX = (i - mid) * 120;
  3. Piloter le prop animate avec isExpanded

    Passe un objet animate à chaque motion.a qui lit isExpanded pour choisir entre les valeurs d'empilement et d'eventail. Ajoute un délai en cascade de i * 0.05 secondes pour que les cartes s'étalent en vague plutôt que de toutes bouger en même temps. Le tableau d'easing custom [0.16, 1, 0.3, 1] produit un départ rapide avec une fin en légère surtension.

    animate={{
      rotate: isExpanded ? fanRotate : stackRotate,
      x: isExpanded ? fanX : 0,
      scale: isExpanded ? 0.88 : 1 - i * 0.02,
      zIndex: items.length - i,
    }}
    transition={{ duration: 0.5, delay: i * 0.05, ease: [0.16, 1, 0.3, 1] }}
  4. Ajouter l'entrée whileInView et le lift whileHover

    Fixe initial à opacity:0, y:40, rotate:0 et whileInView à opacity:1, y:0, rotate:stackRotate pour que les cartes s'animent à l'entrée en scroll indépendamment de l'état de survol. Un simple whileHover de y:-8 soulève légèrement la carte survolée, renforçant la métaphore du jeu de cartes physique. Entoure tout d'AnimatePresence pour gérer le rendu conditionnel proprement.

    initial={{ opacity: 0, y: 40, rotate: 0 }}
    whileInView={{ opacity: 1, y: 0, rotate: stackRotate }}
    whileHover={{ y: -8 }}

Quand l'utiliser

Blog Card Stack fonctionne bien au milieu ou en fin de landing page d'agence, SaaS ou éditoriale, quand on veut mettre en avant 3 à 5 articles récents sans une grille conventionnelle. L'interaction invite à la curiosité sans l'imposer. A éviter sur un index de blog dédié où les lecteurs attendent des lignes scannables, et toujours prévoir un fallback adapté au tactile car le déploiement en eventail ne se déclenche pas sur mobile.

Utilisé par

  • Linear, Utilise des surfaces superposées style carte avec des révélations en cascade dans ses sections de fonctionnalités produit pour créer de la profondeur sans encombrer la mise en page.
  • Stripe, Présente des motifs de jeu de cartes superposées dans ses pages produits développeur pour communiquer plusieurs éléments parallèles sans liste plate.
  • Craft, Emploie des animations de cartes documents empilées sur sa landing page pour traduire l'idée de notes superposées et de pages liées.

FAQ

L'effet eventail fonctionne-t-il sur mobile ?

Non. Le déploiement repose sur les événements mouseenter et mouseleave, qui ne se déclenchent pas sur écran tactile. Sur mobile, garde les cartes dans l'état empilé ou remplace l'interaction par un toggle tap-to-expand.

Combien de cartes la pile peut-elle gérer ?

Le composant découpe le tableau articles aux 5 premiers éléments. Au-delà de 5, les cartes dans l'état empilé commencent à se chevaucher maladroitement et le déploiement en eventail à 120px d'intervalle dépasse le conteneur de 640px. Trois à cinq cartes donne le meilleur résultat visuel.

Pourquoi utiliser AnimatePresence ici ?

AnimatePresence gère le cas où le tableau articles change dynamiquement, garantissant que les cartes supprimées sortent proprement plutôt que de disparaître instantanément. Pour un tableau statique, il ajoute peu de surcharge mais rend le composant robuste pour des scénarios de données temps réel.

Peut-on remplacer l'effet de profondeur par box-shadow par autre chose ?

Oui. Une alternative courante est de varier le filtre de luminosité (filter: brightness(0.9 - i * 0.05)) pour assombrir les cartes plus profondes dans la pile, simulant l'ombre sans ombre portée dure. On peut combiner les deux pour un sens de profondeur physique plus dramatique.

"use client";

import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Calendar, ArrowUpRight } from "lucide-react";

interface Article {
  title: string;
  excerpt: string;
  date: string;
  tag: string;
  image: string;
  url: string;
}

interface BlogCardStackProps {
  heading?: string;
  subtitle?: string;
  articles?: Article[];
}

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

Animation cartes empilées en eventail React, Code + Tutoriel