Retour au catalogue

Values Parallax Cards

Cartes de valeurs avec effet de profondeur parallaxe au mouvement de la souris.

valuesmedium Both Responsive a11y
elegantboldagencyportfoliouniversalgrid
Theme

Cómo crear tarjetas con tilt 3D de paralaje en React

Una tarjeta con tilt 3D en React sigue la posición del ratón relativa a cada tarjeta, la convierte en valores rotateX/rotateY mediante useTransform de Framer Motion, y luego suaviza la rotación con useSpring para que la inclinación siga al cursor con inercia natural y vuelva a plano cuando el ratón sale.

  • Stack: React + Framer Motion + Tailwind v4 + Lucide React, ~90 líneas en total, sin dependencias adicionales.
  • API principal: useMotionValue, useTransform, useSpring, un conjunto de motion values por instancia de tarjeta.
  • perspective: 800 en el div externo, transformStyle: preserve-3d en la tarjeta, translateZ: 40 en la capa de contenido para dar profundidad.
  • Accesible: encabezados h3 semánticos e iconos Lucide renderizados como decorativos (no hace falta aria-label cuando el icono ya va acompañado de texto).
  • En dispositivos táctiles se ve la animación de entrada escalonada pero no el tilt, los eventos de ratón no se disparan al tocar.

Values Parallax Cards es una cuadrícula responsive de tarjetas de valores donde cada tarjeta se inclina en 3D hacia el cursor al pasar el ratón. La capa de contenido flota hacia delante con un desplazamiento translateZ, reforzando la ilusión de profundidad. Las tarjetas aparecen de forma secuencial con un fundido escalonado, y luego responden de manera independiente al movimiento del ratón, cada una sigue sus propias coordenadas de puntero.

Anatomía

La sección envuelve un encabezado centrado (badge opcional, h2, subtítulo) y una cuadrícula responsive de 1-2-3 columnas. Cada celda es una ParallaxCard, un motion.div externo que contiene el contexto de perspectiva y escucha los eventos de ratón, un motion.div interno que recibe rotateX/rotateY y lleva el borde y el fondo, y un tercer motion.div anidado con translateZ: 40 que proyecta el icono, el título y el párrafo hacia el espectador.

Cómo funciona

En cada movimiento del ratón, el handler lee el bounding rect de la tarjeta y calcula valores x/y normalizados en el rango [-0.5, 0.5]. Estos alimentan dos useMotionValue (mouseX y mouseY). useTransform mapea los valores normalizados a ángulos de rotación: mouseY controla rotateX entre 8 y -8 grados, mouseX controla rotateY entre -8 y 8. Cada rotación se envuelve en un useSpring con stiffness 200 y damping 20, de modo que la tarjeta sigue al cursor con un ligero retardo elástico. Cuando el ratón sale, ambos motion values se restablecen a 0 y los springs devuelven la tarjeta a plano.

Cómo crearlo en React

  1. Configurar el contenedor de perspectiva

    Envuelve cada tarjeta en un motion.div con style={{ perspective: 800, transformStyle: 'preserve-3d' }}. Este div es el que captura los eventos de ratón, no rota él mismo, solo proporciona el contexto de proyección 3D para el hijo.

    const ref = useRef<HTMLDivElement>(null);
    const mouseX = useMotionValue(0);
    const mouseY = useMotionValue(0);
    
    // Outer wrapper, holds perspective, captures mouse
    <motion.div ref={ref} style={{ perspective: 800, transformStyle: "preserve-3d" }}
      onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave}>
  2. Derivar los springs de rotación desde la posición del ratón

    Normaliza la posición del ratón a [-0.5, 0.5] relativa a la tarjeta, luego pásala por useTransform para obtener los ángulos de rotación. Envuelve ambos en useSpring para añadir inercia. Los rangos de rotación de ±8 grados resultan sutiles y legibles; sube más solo en tarjetas grandes donde el tilt siga siendo cómodo.

    const rotateX = useSpring(
      useTransform(mouseY, [-0.5, 0.5], [8, -8]),
      { stiffness: 200, damping: 20 }
    );
    const rotateY = useSpring(
      useTransform(mouseX, [-0.5, 0.5], [-8, 8]),
      { stiffness: 200, damping: 20 }
    );
  3. Aplicar rotateX/rotateY a la superficie de la tarjeta

    El motion.div interno recibe los springs de rotación en su style. Mantén transformStyle: 'preserve-3d' aquí también para que la capa de contenido pueda salir del plano de la tarjeta con translateZ. Añade una clase hover:shadow-2xl para la sombra de profundidad que completa el efecto de elevación.

    <motion.div style={{ rotateX, rotateY, transformStyle: "preserve-3d" }}
      className="rounded-xl border p-8 hover:shadow-2xl">
      <motion.div style={{ translateZ: 40 }}>
        {/* icon, title, description */}
      </motion.div>
    </motion.div>
  4. Escalonar la animación de entrada

    Pasa el índice de la tarjeta al delay de transición del motion.div externo: `delay: index * 0.1`. Combínalo con whileInView para que el escalonado se dispare cuando la cuadrícula entra en el viewport al hacer scroll, no al cargar la página. Pon viewport={{ once: true }} para evitar que se repita al volver a subir.

    <motion.div
      initial={{ opacity: 0, y: 24 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true }}
      transition={{ delay: index * 0.1, duration: 0.5 }}
    >

Cuándo usarlo

Usa este patrón en páginas de marca, agencia o portfolio donde quieras que los valores o funcionalidades se sientan premium. El tilt 3D transmite cuidado por el detalle sin visuales pesados. Evítalo en tableros, páginas con muchos datos o en cualquier sitio donde las tarjetas necesiten una interacción de clic, el tilt al pasar el ratón compite con la affordance de clic. En móvil el tilt no está presente, así que asegúrate de que las tarjetas se lean bien en plano. Limita la cuadrícula a 3-6 elementos; con más de seis tarjetas la entrada escalonada se siente lenta.

Usado por

  • Stripe, Usa un sutil tilt 3D en las secciones de funcionalidades del producto para añadir profundidad sin distraer del contenido.
  • Linear, Las tarjetas de funcionalidades del sitio de marketing responden al movimiento del puntero con transformaciones de perspectiva, reforzando la sensación premium.
  • Lottiefiles, Las tarjetas de categorías y funcionalidades usan el tilt con seguimiento del ratón para que la marca centrada en la animación se sienta interactiva de principio a fin.

FAQ

¿Por qué usar una ref para calcular la posición del ratón en lugar de un listener de eventos global?

getBoundingClientRect sobre la ref de la tarjeta te da coordenadas relativas a esa tarjeta concreta, así que el tilt siempre queda centrado en la tarjeta sobre la que está el cursor. Un listener global obligaría a restar manualmente el offset de la tarjeta en la página en cada evento.

¿Por qué envolver useTransform en useSpring en lugar de aplicar el spring a los valores brutos del ratón?

Los propios valores del ratón deben actualizarse al instante, añadir un spring ahí haría que la posición normalizada se retrasara, lo que se ve mal. Aplicar el spring después de la transformación hace que el ángulo de rotación se suavice mientras el seguimiento de las coordenadas se mantiene preciso.

¿Cómo hago que el tilt sea más o menos pronunciado?

Cambia el rango de salida en useTransform: [8, -8] para rotateX y [-8, 8] para rotateY. Los valores entre 5 y 12 grados funcionan bien para tamaños de tarjeta habituales. Pasar de 15 grados tiende a distorsionar la legibilidad del texto. El valor translateZ de la capa de contenido (actualmente 40) también afecta a la profundidad percibida, redúcelo para un aspecto más plano.

¿Funciona el tilt en pantallas táctiles?

No. onMouseMove no se dispara con los eventos táctiles. Las tarjetas siguen teniendo la animación de entrada escalonada y se ven correctas en reposo. Si quieres una versión táctil, escucha deviceorientation y mapea los ángulos de inclinación del dispositivo a los mismos motion values rotateX/rotateY.

"use client";

import React, { useRef } from "react";
import { motion, useMotionValue, useTransform, useSpring } from "framer-motion";
import * as LucideIcons from "lucide-react";

interface ValueItem {
  id: string;
  title: string;
  description: string;
  icon?: string;
}

interface ValuesParallaxCardsProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  values: ValueItem[];
}

function getIcon(name?: string) {
  if (!name) return null;

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Reseñas

Tarjetas 3D tilt para sección de valores en React con Framer