Créer une visionneuse produit 360 draggable en React
Une visionneuse produit 360 en React convertit un offset de glisser horizontal en transform rotateY via useMotionValue et useTransform de Framer Motion, lissé par un useSpring stiffness 200 damping 30. Le spring revient au centre au relâchement, et un rotateY lié au scroll ajoute du mouvement ambiant quand aucun drag n'est actif.
- Stack : React 18 + Framer Motion 11, ~270 lignes, aucune dépendance d'icônes.
- API clé : useMotionValue, useSpring, useTransform, useScroll, tous issus de framer-motion.
- Drag limité à ±200px ; rotateY mappe sur [-25°, 25°] ; scale descend à 0.95 aux extrémités.
- Accessible : l'alt de l'image est requis ; l'interaction drag est un enrichissement progressif.
- Sur mobile le geste drag fonctionne nativement ; le parallaxe scroll se déclenche indépendamment.
Product Showcase 360 est une section React centrée qui permet aux visiteurs de glisser l'image produit gauche-droite pour ressentir une rotation en perspective, simulant la rotation d'un objet réel. Il combine drag contraints, springs et parallaxe scroll de Framer Motion en un composant autonome, utilisable avec n'importe quelle image ou avec un placeholder stylisé si aucune n'est fournie.
Anatomie
Le composant s'articule en quatre blocs empilés dans un conteneur centré. En haut, un header qui se révèle contient le badge (point accent lumineux + libellé), le titre h2 et un sous-titre italique en serif. En dessous, le viewer interactif est une motion.div qui accepte le drag sur l'axe x, enveloppant un conteneur image carré avec coins arrondis et bordure subtile. Une légende d'aide au glisser s'anime avec 500ms de délai. Sous le viewer, un paragraphe de description occupe max-width 520px. En bas, les specs s'affichent dans une CSS grid responsive auto-fit, chaque carte entrant avec un décalage de délai.
Comment ça marche
La rotation est pilotée par trois motion values câblées en séquence. dragX capture l'offset de drag brut à chaque changement (écrit via info.offset.x de onDrag) ; smoothX lui applique un spring (stiffness 200, damping 30) pour que la rotation soit pesante plutôt qu'instantanée ; rotation est ensuite dérivée de smoothX via useTransform, mappant la plage [-200, 200] vers [-25deg, 25deg] sur l'axe rotateY. Une motion value scale issue du même smoothX descend à 0.95 aux extrêmes pour suggérer la profondeur. Au relâchement, onDragEnd remet dragX à 0 et le spring décélère naturellement la rotation vers le centre. Une scrollRotation secondaire, calculée depuis useScroll sur la ref de section, produit un balancement doux de ±15deg au passage, elle n'est visible que sur le placeholder en l'absence d'imageSrc.
Comment le coder en React
Câble la chaîne drag vers rotateY
Crée un useMotionValue pour la position de drag brute, passe-la dans un useSpring pour l'inertie, puis dérive l'angle de rotation avec useTransform. Cette chaîne en trois étapes est le coeur mécanique du composant.
const dragX = useMotionValue(0); const smoothX = useSpring(dragX, { stiffness: 200, damping: 30 }); const rotation = useTransform(smoothX, [-200, 200], [-25, 25]); const scaleVal = useTransform(smoothX, [-200, 0, 200], [0.95, 1, 0.95]);Applique les contraintes de drag et reset au relâchement
Enveloppe l'image dans une motion.div avec drag='x', dragConstraints limitant la plage à ±200px et dragElastic à 0.1 pour un léger rebond en bout de course. Dans onDrag, écris info.offset.x dans dragX ; dans onDragEnd, remets dragX à 0 pour que le spring ramène le produit face caméra.
<motion.div drag="x" dragConstraints={{ left: -200, right: 200 }} dragElastic={0.1} onDrag={(_, info) => dragX.set(info.offset.x)} onDragEnd={() => dragX.set(0)} style={{ rotateY: rotation, scale: scaleVal, perspective: "800px" }} >Ajoute une rotation ambiante liée au scroll
Attache une ref à la section et passe-la à useScroll avec offset ['start end', 'end start']. Mappe le scrollYProgress résultant sur un rotateY ±15deg sur le placeholder ou une couche secondaire. Ça tourne indépendamment du drag et donne vie à la section quand l'utilisateur scrolle sans interagir.
const { scrollYProgress } = useScroll({ target: sectionRef, offset: ["start end", "end start"], }); const scrollRotation = useTransform(scrollYProgress, [0, 1], [-15, 15]);Construis la grille de specs avec entrée en cascade
Rends les specs dans une CSS grid avec gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))'. Chaque carte spec est une motion.div avec fade-up whileInView et un délai de i * 0.08s. Garde le maxWidth de la grille à 600px pour qu'elle reste lisible même à quatre colonnes.
{specs.map((spec, i) => ( <motion.div key={i} initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: i * 0.08, duration: 0.4 }} > <span>{spec.value}</span> <span>{spec.label}</span> </motion.div> ))}
Quand l'utiliser
À utiliser en section d'ouverture sur une landing produit où un seul produit héros mérite une attention tactile : hardware, casques audio, montres, appareils SaaS physiques, packaging premium. L'interaction drag signale immédiatement 'explore-moi' sans aucune instruction. À éviter quand tu présentes un carrousel de plusieurs produits (l'hypothèse d'une image unique ne tient plus), ou sur une page à tunnel de conversion serré où la distraction coûte cher. Sur les appareils très bas de gamme, les springs peuvent paraître lourds ; teste sur du vrai matériel avant de pousser.
Utilisé par
- Apple, Rotation 3D pilotée au scroll des AirPods Max sur les pages produit, référence absolue du storytelling produit par rotation.
- Nothing, Vues produit en perspective avec inclinaison réactive au pointeur sur les pages Phone (2), cohérent avec l'identité de marque hardware tactile.
- Teenage Engineering, Images produit interactives façon 360 pour ses synthés, permettant aux visiteurs d'inspecter les détails hardware avant achat.
- Sonos, Images produit avec parallaxe scroll et légers décalages de perspective pour transmettre la qualité de fabrication premium sur toute la gamme.
FAQ
Puis-je utiliser plusieurs images produit pour simuler de vraies séquences 360 images ?
Ce composant utilise une seule image avec une illusion de perspective rotateY CSS, pas de lecture image par image. Pour de vraies séquences d'images (ex. 36 PNG), tu trackes l'offset de drag, en déduis un index de frame et swappes le src en conséquence, la chaîne de motion values reste identique, mais la source d'image devient réactive au lieu de statique.
Pourquoi la rotation me semble rigide sur ma machine ?
Le ratio stiffness/damping dans useSpring contrôle le ressenti. Baisser la stiffness (ex. 120) la rend plus flottante ; augmenter le damping (ex. 40) absorbe les oscillations plus vite. Les valeurs par défaut (200/30) sont calibrées pour un ressenti réactif avec un retour rapide, ajuste selon la métaphore de poids de ton produit.
Le geste de drag est-il accessible ?
Le drag est un enrichissement progressif ; l'image produit et toutes les specs sont lisibles sans interaction. Il n'existe pas d'équivalent clavier pour la rotation, alors ajoute un aria-label sur la div draggable (ex. 'Glisser pour faire pivoter la vue produit') et assure-toi que l'image a un alt significatif. Les lecteurs d'écran accèdent à l'intégralité du contenu quoi qu'il arrive.
Le parallaxe scroll entre-t-il en conflit avec la rotation drag ?
Non, ils s'appliquent à des éléments différents. Le rotateY piloté par le drag est sur la div draggable extérieure ; le rotateY piloté par le scroll est sur une motion.div imbriquée à l'intérieur du placeholder. Quand imageSrc est fourni, la rotation scroll n'est pas appliquée, donc pas de conflit.