Créer des cartes parallaxe 3D au survol en React
Une carte à tilt 3D en React suit la position de la souris relative à chaque carte, la convertit en valeurs rotateX/rotateY via useTransform de Framer Motion, puis lisse la rotation avec useSpring pour que l'inclinaison suive le curseur avec une inertie naturelle et revienne à plat au départ de la souris.
- Stack : React + Framer Motion + Tailwind v4 + Lucide React, ~90 lignes au total, zéro dépendance supplémentaire.
- API clé : useMotionValue, useTransform, useSpring, un jeu de motion values par instance de carte.
- perspective: 800 sur le div externe, transformStyle: preserve-3d sur la carte, translateZ: 40 sur la couche contenu pour la profondeur.
- Accessible : titres h3 sémantiques, icônes Lucide rendues comme décoratives (pas d'aria-label nécessaire quand l'icône est accompagnée du texte).
- Sur mobile, l'animation d'entrée en stagger s'affiche mais pas le tilt, les événements souris ne se déclenchent pas au toucher.
Values Parallax Cards est une grille responsive de cartes de valeurs où chaque carte s'incline en 3D vers le curseur au survol. La couche contenu flotte en avant grâce à un offset translateZ, renforçant l'illusion de profondeur. Les cartes apparaissent en séquence avec un fondu décalé, puis réagissent indépendamment à la souris, chacune suit ses propres coordonnées de pointeur.
Anatomie
La section enveloppe un header centré (badge optionnel, h2, sous-titre) et une grille responsive 1-2-3 colonnes. Chaque cellule est une ParallaxCard, un motion.div externe qui porte le contexte de perspective et écoute les événements souris, un motion.div interne qui reçoit rotateX/rotateY et porte la bordure et le fond, et un troisième motion.div imbriqué avec translateZ: 40 qui projette l'icône, le titre et le paragraphe vers le spectateur.
Comment ça marche
À chaque mousemove, le handler lit le bounding rect de la carte et calcule des valeurs x/y normalisées dans [-0.5, 0.5]. Elles alimentent deux useMotionValue (mouseX et mouseY). useTransform mappe ces valeurs vers des angles de rotation : mouseY pilote rotateX entre 8 et -8 degrés, mouseX pilote rotateY entre -8 et 8. Chaque rotation est enveloppée dans un useSpring avec stiffness 200 et damping 20, ce qui donne à la carte un léger retard élastique. Au départ de la souris, les deux motion values reviennent à 0 et les springs ramènent la carte à plat.
Comment le coder en React
Mettre en place le conteneur de perspective
Enveloppe chaque carte dans un motion.div avec style={{ perspective: 800, transformStyle: 'preserve-3d' }}. Ce div est celui qui capte les événements souris, il ne tourne pas lui-même, il fournit seulement le contexte de projection 3D pour l'enfant.
const ref = useRef<HTMLDivElement>(null); const mouseX = useMotionValue(0); const mouseY = useMotionValue(0); // Outer wrapper, holds perspective, captures mouse <motion.div ref={ref} style={{ perspective: 800, transformStyle: "preserve-3d" }} onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave}>Dériver les springs de rotation depuis la position souris
Normalise la position de la souris dans [-0.5, 0.5] relative à la carte, puis passe-la dans useTransform pour obtenir les angles de rotation. Enveloppe les deux dans useSpring pour ajouter de l'inertie. Des plages de rotation de ±8 degrés semblent subtiles et lisibles ; monte plus haut seulement pour de grandes cartes où le tilt reste confortable.
const rotateX = useSpring( useTransform(mouseY, [-0.5, 0.5], [8, -8]), { stiffness: 200, damping: 20 } ); const rotateY = useSpring( useTransform(mouseX, [-0.5, 0.5], [-8, 8]), { stiffness: 200, damping: 20 } );Appliquer rotateX/rotateY à la surface de la carte
Le motion.div interne reçoit les springs de rotation dans son style. Garde transformStyle: 'preserve-3d' ici aussi pour que la couche contenu puisse sortir du plan de la carte avec translateZ. Ajoute une classe hover:shadow-2xl pour l'ombre de profondeur qui complète l'effet de soulèvement.
<motion.div style={{ rotateX, rotateY, transformStyle: "preserve-3d" }} className="rounded-xl border p-8 hover:shadow-2xl"> <motion.div style={{ translateZ: 40 }}> {/* icon, title, description */} </motion.div> </motion.div>Décaler l'animation d'entrée
Passe l'index de la carte dans le délai de transition du motion.div externe : `delay: index * 0.1`. Associe-le à whileInView pour que le décalage se déclenche au scroll, pas au chargement. Mets viewport={{ once: true }} pour éviter de rejouer au scroll retour.
<motion.div initial={{ opacity: 0, y: 24 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: index * 0.1, duration: 0.5 }} >
Quand l'utiliser
À utiliser sur des pages marque, agence ou portfolio où tu veux que les valeurs ou fonctionnalités semblent premium. Le tilt 3D signale le soin du détail sans visuels lourds. À éviter sur les tableaux de bord, pages de données denses, ou partout où les cartes ont besoin d'une interaction clic, le tilt au survol entre en concurrence avec l'affordance de clic. Sur mobile le tilt est absent, donc assure-toi que les cartes se lisent bien à plat. Limite la grille à 3-6 éléments ; au-delà de six cartes l'entrée en stagger paraît lente.
Utilisé par
- Stripe, Utilise un léger tilt 3D sur les sections de fonctionnalités produit pour ajouter de la profondeur sans distraire du contenu.
- Linear, Les cartes de fonctionnalités sur le site marketing réagissent au mouvement du pointeur avec des transformations perspective, renforçant la perception premium.
- Lottiefiles, Les cartes de catégories et fonctionnalités utilisent le tilt au suivi souris pour rendre la marque centrée sur l'animation interactive de bout en bout.
FAQ
Pourquoi utiliser une ref pour calculer la position souris plutôt qu'un écouteur global ?
getBoundingClientRect sur la ref de la carte donne des coordonnées relatives à cette carte spécifique, donc le tilt est toujours centré sur la carte survolée. Un écouteur global nécessiterait de soustraire l'offset de la carte dans la page à chaque événement.
Pourquoi envelopper useTransform dans useSpring plutôt qu'appliquer le spring aux valeurs souris brutes ?
Les valeurs souris elles-mêmes doivent se mettre à jour instantanément, ajouter un spring là ferait traîner la position normalisée, ce qui paraît étrange. Appliquer le spring après la transformation signifie que l'angle de rotation s'adoucit pendant que le suivi des coordonnées reste précis.
Comment rendre le tilt plus ou moins prononcé ?
Change la plage de sortie dans useTransform : [8, -8] pour rotateX et [-8, 8] pour rotateY. Des valeurs entre 5 et 12 degrés fonctionnent bien pour des tailles de cartes habituelles. Dépasser 15 degrés tend à déformer la lisibilité du texte. La valeur translateZ sur la couche contenu (actuellement 40) affecte aussi la profondeur perçue, réduis-la pour un rendu plus plat.
Le tilt fonctionne-t-il sur écrans tactiles ?
Non. onMouseMove ne se déclenche pas sur les événements tactiles. Les cartes conservent l'animation d'entrée en stagger et apparaissent correctement au repos. Pour une version tactile, écoute deviceorientation et mappe les angles d'inclinaison de l'appareil vers les mêmes motion values rotateX/rotateY.