Créer une vue produit éclatée au scroll en React
Une vue éclatée pilotée au scroll empile des couches produit en absolu et les écarte au fur et à mesure du défilement, grâce à useScroll de Framer Motion pour lire la progression et useTransform pour la convertir en translateY, translateX et rotateX propres à chaque couche, calculés selon son écart par rapport au centre.
- Stack : React 19 + Framer Motion 11 + custom properties CSS, ~200 lignes, zéro dépendance d'icônes.
- API clé : useScroll (target + offset), useTransform, perspective 3D sur le conteneur.
- Accessible : le label de chaque couche est du texte lisible ; l'animation est décorative et ne masque aucun contenu.
- Responsive : centré avec maxWidth 500px ; fonctionne sur mobile (le scroll est universel, contrairement aux événements pointeur).
- Accepte 3 à 5 couches ; en dessous de 3 l'effet s'affaiblit, au-delà de 5 les chevauchements deviennent gênants.
Product Showcase Exploded est une section pilotée au scroll qui révèle l'architecture d'un produit en séparant ses couches visuelles au fil du défilement. Au repos, tout est empilé de manière compacte ; à mesure que la progression augmente, les couches s'écartent verticalement et latéralement avec une légère inclinaison 3D, le type de révélation qui transforme un texte "faites-nous confiance" en un schéma concret et explorable.
Anatomie
Un header centré (badge, h2, description) apparaît en fondu au premier passage dans le viewport via une motion.div whileInView. En dessous se trouve un conteneur de 500px avec perspective:1200px définie directement dans le style prop. Chaque couche est une motion.div positionnée en absolu (la première en relative pour établir la hauteur du conteneur) contenant une carte au ratio 5:3 avec fond semi-transparent, bordure et label centré. Un court texte d'indication de scroll apparaît sous la pile.
Comment ça marche
L'élément section est passé comme target à useScroll avec l'offset ["start 0.7", "end 0.3"], de sorte que l'animation tourne pendant que la section occupe les 60% centraux du viewport. Pour chaque couche, le code calcule un offset : l'index de la couche moins l'index central du tableau. Cet offset est ensuite utilisé pour dimensionner les sorties de useTransform, les couches au-dessus du centre montent et partent à gauche, celles en dessous descendent et partent à droite. rotateX passe de 0 à -8 degrés dans la première moitié du scroll, donnant à toute la pile une légère inclinaison vers l'avant. L'opacité commence à 0.6 avant la fenêtre de scroll, monte à 1 pendant, et redescend à 0.8 à la fin.
Comment le coder en React
Initialise l'observateur de scroll sur la section
Attache un ref à ton élément section et passe-le à useScroll. Le tableau offset contrôle l'ouverture et la fermeture de la fenêtre d'animation, "start 0.7" signifie que l'animation démarre quand le haut de la section atteint 70% depuis le haut du viewport.
const sectionRef = useRef<HTMLElement>(null); const { scrollYProgress } = useScroll({ target: sectionRef, offset: ["start 0.7", "end 0.3"], });Calcule les motion values par couche depuis l'index central
Dans le map, calcule un offset à partir de la distance de la couche par rapport au centre du tableau. Multiplie cet offset par la distance d'écartement voulue pour obtenir les plages de translateY et translateX. Chaque appel à useTransform produit une motion value indépendante à laquelle Framer Motion s'abonne directement, sans re-render au scroll.
const centerIndex = (totalLayers - 1) / 2; const offset = i - centerIndex; const translateY = useTransform( scrollYProgress, [0, 0.5, 1], [0, offset * 80, offset * 120], ); const translateX = useTransform( scrollYProgress, [0, 0.5, 1], [0, offset * 15, offset * 25], );Ajoute l'inclinaison 3D avec rotateX et perspective
Définis perspective:1200px sur le div conteneur (pas sur les motion.div elles-mêmes) pour que toutes les couches partagent le même espace 3D. Projette scrollYProgress de 0 à 0.5 sur des valeurs rotateX de 0 à -8 degrés. La direction négative incline le haut de la pile vers le spectateur.
const rotateX = useTransform( scrollYProgress, [0, 0.5], [0, -8], ); // On the container: <div style={{ perspective: "1200px" }}>Positionne les couches en absolu, la première en relative
Définis position:'relative' sur la première couche pour que le conteneur prenne sa hauteur, et position:'absolute', top:0, left:0, right:0 sur toutes les suivantes. Ainsi toutes les couches démarrent superposées et le conteneur ne s'effondre pas. Assigne zIndex à totalLayers - i pour que la première couche soit au-dessus.
position: i === 0 ? "relative" : "absolute", top: 0, left: 0, right: 0, zIndex: totalLayers - i,
Quand l'utiliser
Utilise ce pattern quand un produit a des couches techniques clairement distinctes qui valent la peine d'être nommées : plateformes SaaS (UI, API, infra), produits hardware (boîtier, PCB, capteurs), ou outils de design (moteur de rendu, couche plugin, canvas). Il fonctionne mieux dans une section "comment ça marche" ou "technologie" placée tôt à mi-page. Évite-le sur des pages marketing simples où la complexité est un frein, et ne le positionne jamais juste avant ou après un footer ou un formulaire d'auth, l'effet a besoin d'espace et d'un lecteur déjà engagé avec le produit.
Utilisé par
- Stripe, Utilise des illustrations de cartes empilées qui se séparent pour révéler l'architecture du flux de paiement sur ses pages produit.
- Linear, Emploie des révélations de couches pilotées au scroll pour présenter la structure de son moteur de gestion de projets.
- Apple, Les pages produit Mac Pro et iPhone utilisent des animations de couches hardware éclatées pour communiquer la profondeur de l'ingénierie.
- Vercel, Les schémas d'infrastructure sur la page plateforme se séparent en couches distinctes (Edge Network, Functions, Build) à mesure que l'utilisateur scrolle.
FAQ
Pourquoi appeler useTransform dans la boucle map plutôt que de calculer les valeurs en dehors ?
Chaque couche a besoin de sa propre motion value indépendante, useTransform crée une valeur dérivée à laquelle Framer Motion s'abonne directement sans déclencher de re-renders React. L'appel par couche dans le map est intentionnel ; les violations des règles des hooks ne s'appliquent pas ici car la longueur du tableau de couches est fixe au moment du rendu.
Comment contrôler l'écartement entre les couches ?
L'écartement est piloté par le multiplicateur dans les tableaux de sortie de useTransform : offset * 80 pour le Y à mi-scroll et offset * 120 pour le Y en fin de scroll. Augmente ces valeurs pour écarter davantage les couches, diminue-les pour une séparation plus subtile. Les multiplicateurs X (15 et 25) ajoutent une légère diagonale qui évite que les couches ressemblent à une pile plate.
Est-ce que ça fonctionne sans Framer Motion ?
Tu peux reproduire la lecture du scroll avec un écouteur d'événement scroll classique et des custom properties CSS, mais tu perds le scheduling rAF optimisé et le chemin de mutation DOM direct de Framer Motion. L'écart se ressent à 60fps sur des appareils d'entrée de gamme, reste sur Framer Motion ou envisage la Web Animations API comme alternative plus légère.
Quel est le nombre minimal de couches pour que l'effet soit lisible ?
Trois couches est le minimum pratique. Avec deux, la séparation ressemble à un simple glissement plutôt qu'à une révélation structurelle. Quatre est le bon équilibre pour la plupart des produits ; cinq convient aux présentations techniques approfondies mais exige une différenciation chromatique soignée entre couches adjacentes pour qu'elles ne fusionnent pas visuellement quand elles sont empilées.