Créer une grille bento avec tilt magnétique 3D en React
Une grille bento magnétique en React exploite useMotionValue et useSpring de Framer Motion pour convertir la position du pointeur dans chaque carte en valeurs rotateX/rotateY, créant un tilt 3D. La carte survolée monte légèrement en scale et les autres s'estompent à 0.6 pour former un spotlight sur la grille.
- Stack : React 18 + Framer Motion 11 + Lucide React, ~147 lignes, zéro dépendance supplémentaire.
- API clé : useMotionValue, useSpring, useTransform, angle de tilt ±8 degrés, spring stiffness 220 / damping 28.
- Mise en page : CSS grid-template-areas 3 colonnes × 3 rangées, 6 cellules sur 5 zones nommées (a, b, c, d, e, f).
- Cinq variantes de cellule : stat (grand chiffre + badge delta), feature (icône + texte), quote, visual (blob dégradé animé), CTA (bouton).
- Attention mobile : le tilt 3D nécessite un pointeur. Sur écran tactile les cartes s'affichent correctement mais sans effet magnétique.
Bento Magnetic Cards est une section grille asymétrique où chaque cellule se penche physiquement vers le curseur, comme une carte attirée par un aimant. Il combine un layout CSS grid-template-areas avec des springs Framer Motion indépendants par carte, pour donner aux pages produit une texture interactive concrète. Pratique pour présenter des contenus hétérogènes (métriques, features, citations, CTA) dans un seul bloc visuel.
Anatomie
La section enveloppe un header centré (badge, h2, sous-titre) et une grille CSS 3×3. Six composants MagneticCell occupent les zones de grille a à f, deux cellules s'étendent sur deux colonnes chacune, créant l'asymétrie. Chaque cellule porte une des cinq variantes de contenu (stat, feature, quote, visual, cta) rendu par un composant CellContent sans état. Un seul state hoveredId au niveau de la section pilote l'estompage des cartes inactives.
Comment ça marche
Dans chaque MagneticCell, deux motion values suivent le décalage normalisé du curseur dans la carte : mx et my varient de -0.5 à 0.5. useTransform les mappe vers rotateX/rotateY (±8 degrés), et useSpring lisse les deux avec la même config (stiffness 220, damping 28, mass 0.6). Un spring séparé sur scale passe de 1 à 1.025 à l'entrée. Au mouseleave, toutes les valeurs reviennent à 0 et la carte se redresse via le même spring. La cellule visual ajoute une animation secondaire : son blob dégradé monte à scale 1.15 et tourne de 15 degrés au survol, via la prop animate de Framer Motion avec la courbe EASE custom.
Comment le coder en React
Prépare la grille asymétrique
Définis la mise en page avec grid-template-areas pour que les tailles de cellules varient sans media queries. Assigne à chaque BentoCell un string area qui correspond au template. Trois rangées et trois colonnes suffisent pour six cellules dont deux s'étendent sur deux colonnes.
style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gridTemplateAreas: `"a a b" "c d d" "e e f"`, gap: "0.875rem", }}Suis la position normalisée du curseur dans chaque carte
Au mousemove, calcule le décalage X et Y du curseur par rapport à la carte via getBoundingClientRect, puis divise par width/height pour obtenir une valeur entre 0 et 1. Soustrais 0.5 pour que le centre mappe à 0 et les bords à ±0.5. Écris ces valeurs dans deux instances useMotionValue.
const mx = useMotionValue(0); const my = useMotionValue(0); function handleMove(e: React.MouseEvent) { const r = ref.current?.getBoundingClientRect(); if (!r) return; mx.set((e.clientX - r.left) / r.width - 0.5); my.set((e.clientY - r.top) / r.height - 0.5); }Convertis le décalage en rotateX/Y via des springs
Passe mx et my dans useTransform pour produire les angles de rotation, puis enveloppe chacun dans un useSpring pour l'inertie. Applique rotateX et rotateY sur le motion.div avec perspective:'900px' et transformStyle:'preserve-3d' pour que le rendu 3D soit correct.
const SPRING = { stiffness: 220, damping: 28, mass: 0.6 }; const rotateX = useSpring(useTransform(my, [-0.5, 0.5], [8, -8]), SPRING); const rotateY = useSpring(useTransform(mx, [-0.5, 0.5], [-8, 8]), SPRING); const scale = useSpring(1, SPRING); // on enter scale.set(1.025); // on leave mx.set(0); my.set(0); scale.set(1);Ajoute le spotlight de survol au niveau de la grille
Remonte un state hoveredId de type string au composant section parent. Chaque MagneticCell appelle onHover(cell.id) à l'entrée et onHover(null) à la sortie. Les cartes dont hoveredId est défini mais ne correspond pas à leur propre id s'animent vers opacity 0.6 via la prop animate de Framer Motion, créant un effet de focus sur toute la grille sans manipulation de classes CSS.
Quand l'utiliser
Utilise cette section quand tu veux présenter les points forts d'un produit (une métrique, une feature, un témoignage, un CTA) en une seule déclaration visuelle, landings SaaS, vitrines d'agences, lancements d'outils IA. La diversité des cellules maintient l'intérêt du scan. Évite-la sur les pages éditoriales denses ou les dashboards où la densité d'information prime sur le soin visuel. Comme le tilt est piloté par le pointeur, prévois un fallback statique propre pour les visiteurs sur mobile.
Utilisé par
- Vercel, Utilise des mises en page grille bento asymétriques pour présenter les capacités produit, métriques et preuves sociales sur sa homepage.
- Linear, Combine des cartes feature de tailles variées avec de subtils effets de profondeur au survol pour mettre en valeur les capacités produit sur ses pages marketing.
- Stripe, Utilise des mises en page grille multi-cellules mixant stats, mises en avant de features et CTA dans une seule section sur ses pages produit et feature.
- Loom, Emploie des grilles feature de style bento avec des tailles de carte variées et des états de survol interactifs pour communiquer la valeur produit sur les landing pages.
FAQ
Le tilt magnétique fonctionne-t-il sur écran tactile ?
Non. L'effet s'appuie sur des événements mousemove que les écrans tactiles n'envoient pas. Les cartes s'affichent correctement sur mobile, elles restent simplement plates. Prévois un layout statique comme expérience mobile et conditionne les motion values à une media query pointer ou un check window.matchMedia('(pointer: fine)').
Comment modifier l'intensité du tilt ?
Ajuste le range de sortie dans les appels useTransform. Par défaut, un décalage ±0.5 produit ±8 degrés. Passer [-12, 12] pour un tilt plus dramatique ou [-4, 4] pour un effet subtil est le seul changement nécessaire. Couple-le à un amortissement plus élevé (ex. 35) si la carte paraît trop rebondissante à des angles importants.
Peut-on ajouter des cellules ou changer la disposition de la grille ?
Oui. La grille est entièrement pilotée par CSS grid-template-areas et le champ area de chaque BentoCell. Redéfinis le string de template et mets à jour les valeurs area dans ton tableau de données pour obtenir n'importe quel layout sans toucher à la logique du composant. Veille à ce que les zones remplissent toutes les colonnes de chaque rangée pour éviter les trous.
Pourquoi chaque carte a-t-elle ses propres motion values plutôt qu'un set partagé ?
Le tilt est relatif au bounding box propre de la carte, pas à la page. Chaque MagneticCell normalise les coordonnées du pointeur par rapport à son propre ref, donc l'effet se centre correctement quelle que soit la position de la carte dans la grille. Partager un seul set de valeurs ferait réagir toutes les cartes à la même position absolue du curseur, cassant le centrage par carte.