Retour au catalogue

Gallery 3D Wall

Mur de photos en perspective 3D avec tilt reactif au curseur. Les images reagissent au mouvement de la souris creant une experience immersive.

gallerycomplex Both Responsive a11y
boldelegantagencyportfoliobeautygrid
Theme

Cómo crear un grid de galería 3D con tilt en React con Framer Motion

Una galería 3D con tilt en React envuelve un grid CSS dentro de un contenedor con perspectiva, luego lee la posición del ratón en mousemove para controlar rotateX y rotateY mediante springs de Framer Motion. Todo el grid rota en bloque como un plano rígido, dando profundidad a cada tarjeta sin listeners por tarjeta.

  • Stack: React 18+, Framer Motion 11, lucide-react para los iconos de placeholder, ~170 líneas en total.
  • APIs clave: useMotionValue, useSpring (stiffness 120, damping 20), CSS perspective: 1200px, transformStyle: preserve-3d.
  • Las tarjetas entran en el viewport mediante animaciones whileInView individuales (fundido en el eje Z, retardo escalonado de 50ms por índice).
  • Sin lógica de tilt por tarjeta: un único par de springs controla todo el plano del grid, manteniendo el componente ligero.
  • En dispositivos táctiles el tilt simplemente se queda en 0,0, sin fallback necesario, el layout sigue siendo totalmente funcional.

Gallery 3D Wall es un grid de imágenes responsive que reacciona al cursor del ratón inclinando todo el plano del grid en el espacio 3D. En lugar de aplicar efectos hover tarjeta por tarjeta, todo el layout pivota alrededor de un punto de fuga común, haciendo que la colección parezca un panel físico que el usuario examina desde distintos ángulos. Encaja con portfolios, escaparates de agencia y cualquier página de producto que necesite una sensación inmediata de oficio.

Anatomía

El componente se estructura en tres capas. Una etiqueta section exterior gestiona el padding a todo el ancho y los tokens de fondo. Dentro hay un contenedor con ancho limitado y una cabecera centrada (subtítulo en mayúsculas, h2 grande). Debajo, una div marcada como contenedor de perspectiva aloja una motion.div que actúa como plano 3D; esta motion.div renderiza el grid CSS con columnas auto-fill (minWidth 240px). Cada celda es otra motion.div con su propia entrada whileInView, que envuelve una tarjeta con aspect-ratio fijo (4/3) con color de fondo, border-radius, sombra y un icono de placeholder centrado dentro.

Cómo funciona

En mousemove sobre el contenedor de perspectiva, el handler normaliza la posición del cursor a un rango -0,5/+0,5 relativo a las dimensiones del contenedor usando getBoundingClientRect. Luego multiplica el desplazamiento horizontal por 8 para obtener el ángulo de rotación Y y el desplazamiento vertical por -6 para el ángulo de rotación X. Ambos valores se envían a springs de Framer Motion (stiffness 120, damping 20, mass 0,8) para que el grid siga al cursor con un retardo y un rebote naturales. En mouseleave ambos springs vuelven a cero. El CSS perspective: 1200px en el wrapper y transformStyle: preserve-3d en el grid hacen visible el tilt; sin preserve-3d la rotación no tiene profundidad visual.

Cómo crearlo en React

  1. Montar el wrapper de perspectiva y las motion values

    Crea una div contenedora con perspective: 1200px. Dentro, renderiza una motion.div con style={{ rotateX, rotateY, transformStyle: 'preserve-3d' }}. Conecta rotateX y rotateY a instancias de useSpring para que cualquier cambio se anime de forma suave.

    const SPRING = { stiffness: 120, damping: 20, mass: 0.8 };
    const rotateX = useSpring(useMotionValue(0), SPRING);
    const rotateY = useSpring(useMotionValue(0), SPRING);
  2. Leer y normalizar la posición del ratón

    Asocia onMouseMove al contenedor de perspectiva. Resta el bounding rect del contenedor para obtener una posición local, luego divide por width/height para obtener valores entre 0 y 1. Desplaza en -0,5 para que el centro sea 0,0. Multiplica por tu escala de rotación y envíalo a los springs.

    function handleMouseMove(e: React.MouseEvent<HTMLDivElement>) {
      const rect = containerRef.current?.getBoundingClientRect();
      if (!rect) return;
      const cx = (e.clientX - rect.left) / rect.width - 0.5;
      const cy = (e.clientY - rect.top) / rect.height - 0.5;
      rotateY.set(cx * 8);
      rotateX.set(cy * -6);
    }
  3. Reiniciar al salir el ratón

    Añade un handler onMouseLeave que ponga ambos springs de nuevo a 0. El spring de Framer Motion animará el retorno para que el grid vuelva suavemente en lugar de hacerlo de golpe, manteniendo la sensación física.

    function handleMouseLeave() {
      rotateX.set(0);
      rotateY.set(0);
    }
  4. Escalonar la entrada de las tarjetas con whileInView

    Envuelve cada celda del grid en una motion.div con initial={{ opacity: 0, z: -60, scale: 0.9 }} y whileInView={{ opacity: 1, z: 0, scale: 1 }}. Pasa delay: index * 0.05 en la transition para que las tarjetas aparezcan en cascada en lugar de todas a la vez.

    <motion.div
      initial={{ opacity: 0, z: -60, scale: 0.9 }}
      whileInView={{ opacity: 1, z: 0, scale: 1 }}
      viewport={{ once: true, margin: "-40px" }}
      transition={{ duration: 0.65, delay: i * 0.05, ease: EASE }}
      style={{ transformStyle: "preserve-3d" }}
    >

Cuándo usarlo

Este patrón brilla en las homepages de portfolio, los escaparates de agencia y las galerías de producto donde quieres que el propio grid se sienta vivo. Úsalo cuando la colección tenga entre 6 y 12 elementos, con menos se ve vacío bajo el efecto de tilt, con más la profundidad resulta menos legible. Evítalo en las páginas de categoría de e-commerce donde los usuarios recorren decenas de artículos rápidamente; un grid estático es más rápido de analizar. En móvil el tilt no hace nada, así que asegúrate de que el layout estático sea sólido por sí solo.

Usado por

  • Awwwards, Muestra portfolios de agencias que utilizan habitualmente grids en perspectiva 3D como interacciones distintivas en sitios premiados.
  • Dribbble, Las páginas de portfolio de diseñadores usan con frecuencia layouts de grid con tilt al pasar el ratón para mostrar sus colecciones con profundidad.
  • Resend, Usa sutiles desplazamientos de perspectiva 3D en los grids de tarjetas de su sitio de marketing para aportar un carácter táctil.
  • Basement Studio, Sitio de agencia construido en torno a layouts de grid 3D reactivos al ratón como firma visual central.

FAQ

¿Por qué el tilt no tiene ningún efecto en móvil?

Las pantallas táctiles disparan eventos touch, no mousemove, así que los springs nunca reciben nuevos valores y se quedan en 0,0. El grid se sigue renderizando correctamente como un layout plano; simplemente pierdes la rotación 3D. Si quieres algo en móvil, considera un handler touch-drag que lea el delta de un evento touchmove.

¿Puedo aplicar el tilt por tarjeta en lugar de a todo el grid?

Sí. Mueve la lógica de useMotionValue y useSpring dentro de un componente wrapper de tarjeta y asocia onMouseMove/onMouseLeave por tarjeta. El enfoque por tarjeta consume más CPU con muchos elementos, pero da a cada tarjeta una profundidad independiente. Para galerías de más de 8 tarjetas, el enfoque de grid completo de este componente es la opción más segura.

¿Cuál es el papel de transformStyle: preserve-3d?

Sin preserve-3d, los elementos hijos se aplanan en el plano del padre; la rotación se convierte en un skew 2D que pierde toda la profundidad. Establecer transformStyle: preserve-3d tanto en la motion.div wrapper como en cada div de tarjeta indica al navegador que mantenga los hijos en el espacio 3D para que la cámara de perspectiva vea una profundidad real entre ellos.

¿Cómo reemplazo las tarjetas placeholder por imágenes reales?

Añade un campo src a la interfaz GalleryItem y sustituye la div placeholder por un componente Next.js Image (o una etiqueta img). Mantén el contenedor con aspect-ratio y overflow: hidden, ya que conservan la forma de la tarjeta independientemente de las dimensiones naturales de la imagen.

"use client";

import { motion, useMotionValue, useSpring } from "framer-motion";
import { Image } from "lucide-react";
import { useRef } from "react";

interface GalleryItem {
  label: string;
  color: string;
}

interface Gallery3dWallProps {
  title?: string;
  subtitle?: string;
  items?: GalleryItem[];
}

const EASE = [0.16, 1, 0.3, 1] as const;
const SPRING = { stiffness: 120, damping: 20, mass: 0.8 };

export default function Gallery3dWall({
  title = "Galerie",

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Reseñas

Grid de galería 3D React con tilt del ratón, Framer Motion