Cómo crear un visor de producto 360 con drag en React
Un visor de producto 360 en React convierte un offset de arrastre horizontal en una transform rotateY mediante useMotionValue y useTransform de Framer Motion, suavizado por un useSpring con stiffness 200 y damping 30. El spring vuelve al centro al soltar, y un rotateY vinculado al scroll añade movimiento ambiental cuando no hay ningún drag activo.
- Stack: React 18 + Framer Motion 11, ~270 líneas, cero dependencias de iconos.
- API principal: useMotionValue, useSpring, useTransform, useScroll, todos de framer-motion.
- Drag limitado a ±200px; rotateY se mapea a [-25°, 25°]; scale baja a 0.95 en los extremos.
- Accesible: el texto alt de la imagen es obligatorio; la interacción de drag es una mejora progresiva.
- En dispositivos táctiles el gesto de drag funciona de forma nativa; el parallax de scroll se dispara de forma independiente.
Product Showcase 360 es una sección de React centrada que permite a los visitantes arrastrar la imagen del producto de izquierda a derecha para sentir una rotación en perspectiva, simulando el giro de un objeto real. Combina drag constraints, física de springs y parallax de scroll de Framer Motion en un único componente autónomo, que funciona con cualquier fuente de imagen o con un placeholder estilizado cuando no se proporciona ninguna.
Anatomía
El componente se estructura en cuatro bloques apilados dentro de un único contenedor centrado. Arriba, un header con fade-in contiene el badge (punto de acento luminoso + etiqueta), el título h2 y un subtítulo en cursiva con tipografía serif. Debajo, el visor interactivo es una motion.div que acepta drag en el eje x, envolviendo un contenedor de imagen con aspect-ratio cuadrado, esquinas redondeadas y un borde sutil. Un texto de ayuda al arrastrar aparece con 500ms de retraso. Bajo el visor, un párrafo de descripción se sitúa a max-width 520px. Al final, las specs se renderizan en una CSS grid responsive con columnas auto-fit, y cada tarjeta aparece con un retraso escalonado.
Cómo funciona
La rotación se controla mediante tres motion values conectados en secuencia. dragX captura el offset de arrastre en bruto a medida que cambia (escrito mediante info.offset.x de onDrag); smoothX le aplica un spring (stiffness 200, damping 30) para que la rotación se sienta con peso en lugar de instantánea; rotation se deriva luego de smoothX mediante useTransform, mapeando el rango de arrastre [-200, 200] a [-25deg, 25deg] en el eje rotateY. Una motion value scale, procedente del mismo smoothX, baja a 0.95 en los extremos para sugerir profundidad. Cuando el usuario suelta, onDragEnd resetea dragX a 0 y el spring desacelera de forma natural la rotación de vuelta al centro. Una scrollRotation secundaria, calculada desde useScroll sobre la ref de la sección, produce un balanceo suave de ±15deg cuando el usuario pasa por delante, y solo se muestra en el placeholder cuando no se proporciona imageSrc.
Cómo crearlo en React
Conecta la cadena de transform de drag a rotateY
Crea un useMotionValue para la posición de arrastre en bruto, pásalo por un useSpring para la inercia y luego deriva el ángulo de rotación con useTransform. Esta cadena de tres pasos es todo el núcleo mecánico del componente.
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]);Aplica las drag constraints y resetea al soltar
Envuelve la imagen en una motion.div con drag='x', dragConstraints que bloquea el rango a ±200px y dragElastic a 0.1 para que el límite rebote ligeramente. En onDrag, escribe info.offset.x en dragX; en onDragEnd, resetea dragX a 0 para que el spring devuelva el producto de frente.
<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" }} >Añade una rotación ambiental vinculada al scroll
Adjunta una ref a la sección y pásala a useScroll con offset ['start end', 'end start']. Mapea el scrollYProgress resultante a un rotateY de ±15deg sobre el placeholder o una capa secundaria. Esto se ejecuta de forma independiente del drag y da vida a la sección cuando el usuario pasa haciendo scroll sin interactuar.
const { scrollYProgress } = useScroll({ target: sectionRef, offset: ["start end", "end start"], }); const scrollRotation = useTransform(scrollYProgress, [0, 1], [-15, 15]);Construye la grid de specs con entrada escalonada
Renderiza las specs en una CSS grid con gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))'. Cada tarjeta de spec es una motion.div con fade-up whileInView y un retraso de i * 0.08s. Mantén el maxWidth de la grid en 600px para que se lea bien incluso con cuatro columnas.
{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> ))}
Cuándo usarlo
Úsalo como sección de apertura en una landing de producto donde un único producto héroe merece atención táctil: hardware, auriculares, relojes, dispositivos SaaS físicos, packaging premium. La interacción de drag comunica de inmediato 'explórame' sin ninguna instrucción. Evítalo cuando tienes un carrusel de varios productos (la premisa de una sola imagen deja de funcionar), o cuando la página es crítica para la conversión con un embudo ajustado donde la distracción sale cara. En dispositivos de muy baja gama la física de springs puede sentirse lenta; prueba en hardware real antes de publicar.
Usado por
- Apple, Rotación 3D controlada por scroll de los AirPods Max en las páginas de producto, que establece el estándar del storytelling de producto con drag para rotar.
- Nothing, Vistas de producto en perspectiva con inclinación reactiva al puntero en las landing pages del Phone (2), en línea con la identidad de marca de hardware táctil.
- Teenage Engineering, Imágenes de producto interactivas estilo 360 para sus sintetizadores, que permiten a los visitantes inspeccionar los detalles del hardware antes de comprar.
- Sonos, Imágenes de producto con parallax de scroll y sutiles cambios de perspectiva para transmitir la calidad de fabricación premium en toda la gama de altavoces.
FAQ
¿Puedo usar varias imágenes de producto para simular secuencias reales de 360 fotogramas?
Este componente usa una sola imagen con una ilusión de perspectiva rotateY en CSS, no reproducción fotograma a fotograma. Para secuencias de fotogramas reales (por ejemplo, 36 PNG), rastrearías el offset de arrastre, derivarías de él un índice de fotograma y cambiarías el src en consecuencia: la cadena de motion values se mantiene igual, pero la fuente de la imagen pasa a ser reactiva en lugar de estática.
¿Por qué la rotación se siente rígida en mi máquina?
La relación stiffness/damping en useSpring controla la sensación. Bajar la stiffness (por ejemplo, 120) la hace más flotante; subir el damping (por ejemplo, 40) absorbe la oscilación más rápido. Los valores por defecto (200/30) están ajustados para una sensación reactiva con asentamiento rápido; ajústalos según la metáfora de peso de tu producto.
¿Es accesible el gesto de drag?
El drag es una mejora progresiva; la imagen del producto y todas las specs son totalmente legibles sin interactuar. No hay un equivalente de teclado para la rotación en sí, así que añade un aria-label en la div draggable (por ejemplo, 'Arrastra para rotar la vista del producto') y asegúrate de que la imagen tenga un atributo alt significativo. Los lectores de pantalla acceden al contenido completo en cualquier caso.
¿El parallax de scroll entra en conflicto con la rotación de drag?
No, se aplican a elementos distintos. El rotateY controlado por el drag está en la div draggable exterior; el rotateY controlado por el scroll está en una motion.div anidada dentro del placeholder. Cuando se proporciona un imageSrc, la rotación de scroll no se aplica en absoluto, así que no hay conflicto.