Retour au catalogue

Bento Magnetic Cards

Grille bento asymetrique (grid-template-areas) avec 6 cellules de tailles variees. Chaque cellule reagit au hover avec un tilt magnetique (rotateX/Y ±8deg, useSpring). La cellule survole monte en z-index + scale 1.025, les autres s'estompent a 0.6. Contenu heterogene : stat, feature, quote, blob visuel, CTA.

bentocomplex Both Responsive a11y
boldeleganteditorialsaasagencyuniversalgridasymmetric
Theme

Cómo crear un grid bento con tilt magnético 3D en React

Un grid bento magnético en React usa useMotionValue y useSpring de Framer Motion para convertir la posición del puntero dentro de cada tarjeta en valores rotateX/rotateY, produciendo un efecto tilt 3D. La tarjeta enfocada aumenta ligeramente su scale y atenuar las demás a 0.6 de opacidad crea un foco de atención sobre todo el grid.

  • Stack: React 18 + Framer Motion 11 + Lucide React, ~147 líneas, cero dependencias adicionales.
  • API principal: useMotionValue, useSpring, useTransform, ángulo de tilt ±8 grados, spring stiffness 220 / damping 28.
  • Disposición del grid: CSS grid-template-areas con 3 columnas y 3 filas, 6 celdas repartidas en 5 áreas con nombre (a, b, c, d, e, f).
  • Cinco variantes de celda: stat (cifra grande + badge de delta), feature (icono + texto), quote, visual (blob de degradado animado), CTA (botón).
  • Advertencia en móvil: el tilt 3D requiere un puntero. En dispositivos táctiles las tarjetas se muestran correctamente pero sin el efecto magnético.

Bento Magnetic Cards es una sección de grid asimétrico donde cada celda se inclina físicamente hacia el cursor, como una tarjeta atraída por un imán. Combina una disposición CSS grid-template-areas con springs de Framer Motion independientes por tarjeta, para dar a las páginas de producto una textura interactiva tangible. Útil para mostrar contenidos heterogéneos (métricas, features, citas, CTA) en un único bloque visual.

Anatomía

La sección envuelve una cabecera centrada (badge, h2, subtítulo) y un grid CSS 3×3. Seis componentes MagneticCell ocupan las áreas de grid de la a a la f, dos celdas abarcan dos columnas cada una, lo que da al layout su asimetría. Cada celda lleva una de las cinco variantes de contenido (stat, feature, quote, visual, cta) renderizada por un componente CellContent sin estado. Un único state hoveredId a nivel de la sección controla la atenuación de las tarjetas inactivas.

Cómo funciona

Dentro de cada MagneticCell, dos motion values siguen el desplazamiento normalizado del cursor dentro de la tarjeta: mx y my varían de -0.5 a 0.5. useTransform los mapea a rotateX/rotateY (±8 grados), y useSpring suaviza ambos con la misma config de spring (stiffness 220, damping 28, mass 0.6). Un spring aparte sobre scale pasa de 1 a 1.025 al entrar. Al mouseleave, todos los valores vuelven a 0 y la tarjeta se endereza con el mismo spring. La celda visual añade una animación secundaria: su blob de degradado aumenta a scale 1.15 y gira 15 grados al pasar el cursor, mediante la prop animate de Framer Motion con la curva EASE personalizada.

Cómo crearlo en React

  1. Configura el grid asimétrico

    Define la disposición con grid-template-areas para que los tamaños de celda varíen sin media queries. Asigna a cada BentoCell un string area que coincida con el template. Tres filas y tres columnas bastan para seis celdas, de las cuales dos abarcan dos columnas.

    style={{
      display: "grid",
      gridTemplateColumns: "repeat(3, 1fr)",
      gridTemplateAreas: `"a a b" "c d d" "e e f"`,
      gap: "0.875rem",
    }}
  2. Sigue la posición normalizada del cursor dentro de cada tarjeta

    En mousemove, calcula el desplazamiento X e Y del cursor respecto a la tarjeta con getBoundingClientRect, luego divide por width/height para obtener un valor entre 0 y 1. Resta 0.5 para que el centro se mapee a 0 y los bordes a ±0.5. Escribe estos valores en dos instancias de useMotionValue.

    const mx = useMotionValue(0);
    const my = useMotionValue(0);
    
    function handleMove(e: React.MouseEvent) {
      const r = ref.current?.getBoundingClientRect();
      if (!r) return;
      mx.set((e.clientX - r.left) / r.width - 0.5);
      my.set((e.clientY - r.top) / r.height - 0.5);
    }
  3. Convierte el desplazamiento del cursor en rotateX/Y con springs

    Pasa mx y my por useTransform para producir los ángulos de rotación, luego envuelve cada uno en un useSpring para una inercia suave. Aplica rotateX y rotateY sobre el motion.div junto con perspective:'900px' y transformStyle:'preserve-3d' para que el efecto 3D se renderice correctamente.

    const SPRING = { stiffness: 220, damping: 28, mass: 0.6 };
    const rotateX = useSpring(useTransform(my, [-0.5, 0.5], [8, -8]), SPRING);
    const rotateY = useSpring(useTransform(mx, [-0.5, 0.5], [-8, 8]), SPRING);
    const scale   = useSpring(1, SPRING);
    
    // on enter
    scale.set(1.025);
    // on leave
    mx.set(0); my.set(0); scale.set(1);
  4. Añade el foco de hover a nivel del grid

    Eleva un state hoveredId de tipo string a la sección padre. Cada MagneticCell llama a onHover(cell.id) al entrar y a onHover(null) al salir. Las tarjetas cuyo hoveredId está definido pero no coincide con su propio id se animan a opacity 0.6 mediante la prop animate de Framer Motion, creando un efecto de foco sobre todo el grid sin malabares de clases CSS.

Cuándo usarlo

Recurre a esta sección cuando quieras presentar los puntos clave de un producto (una métrica, una feature, un testimonio, un CTA) juntos en una sola declaración visual, landings SaaS, escaparates de agencias, lanzamientos de herramientas de IA. La diversidad de celdas mantiene interesante el escaneo. Evítala en páginas editoriales densas o dashboards donde la densidad de información importa más que el cuidado visual. Como el tilt se controla con el puntero, prevé un fallback estático limpio para los visitantes en móvil.

Usado por

  • Vercel, Usa disposiciones de grid de estilo bento asimétrico para mostrar las capacidades del producto, métricas y prueba social en su homepage.
  • Linear, Combina tarjetas feature de tamaños variados con sutiles efectos de profundidad al pasar el cursor para destacar las capacidades del producto en sus páginas de marketing.
  • Stripe, Usa disposiciones de grid multi-celda que mezclan stats, destacados de features y CTA dentro de una sola sección en sus páginas de producto y feature.
  • Loom, Emplea grids de features de estilo bento con tamaños de tarjeta variados y estados de hover interactivos para comunicar el valor del producto en las landing pages.

FAQ

¿Funciona el tilt magnético en pantallas táctiles?

No. El efecto se apoya en eventos mousemove que los dispositivos táctiles no disparan. Las tarjetas se renderizan y se ven correctas en móvil, simplemente quedan planas. Prevé un layout estático como experiencia móvil y condiciona los motion values a una media query pointer o a un check window.matchMedia('(pointer: fine)').

¿Cómo cambio la intensidad del tilt?

Ajusta el rango de salida en las llamadas a useTransform. Por defecto, un desplazamiento ±0.5 produce ±8 grados. Pasar [-12, 12] para un tilt más marcado o [-4, 4] para uno sutil es el único cambio necesario. Acompáñalo de un valor de damping más alto (p. ej. 35) si la tarjeta se siente demasiado rebotona en ángulos altos.

¿Puedo añadir más celdas o cambiar la disposición del grid?

Sí. El grid está controlado por completo por CSS grid-template-areas y el campo area de cada BentoCell. Redefine el string del template y actualiza los valores area en tu array de datos para obtener cualquier layout sin tocar la lógica del componente. Procura que las áreas rellenen todas las columnas de cada fila para evitar huecos vacíos.

¿Por qué cada tarjeta necesita sus propios motion values en vez de un set compartido?

El tilt es relativo al bounding box propio de la tarjeta, no a la página. Cada MagneticCell normaliza las coordenadas del puntero respecto a su propio ref, así el efecto se centra correctamente sin importar dónde se ubique la tarjeta en el grid. Compartir un único set de valores haría que todas las tarjetas reaccionaran a la misma posición absoluta del cursor, rompiendo el centrado por tarjeta.

"use client";

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

export type CellVariant = "stat" | "feature" | "quote" | "visual" | "cta";

export interface BentoCell {
  id: string; variant: CellVariant; area: string;
  statValue?: string; statLabel?: string; statDelta?: string;
  featureTitle?: string; featureDesc?: string; featureIconName?: string;
  quoteText?: string; quoteAuthor?: string; quoteRole?: string;
  visualLabel?: string;
  ctaTitle?: string; ctaDesc?: string; ctaLabel?: string;
}

interface Props { badge?: string; title?: string; subtitle?: string; cells: BentoCell[] }

const SPRING = { stiffness: 220, damping: 28, mass: 0.6 };
const EASE = [0.16, 1, 0.3, 1] as const;
const P = "1.75rem";

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Reseñas

Grid bento React con tarjetas tilt 3D, Framer Motion