Cómo crear un grid bento con un spotlight que sigue al ratón en React
Un grid bento con spotlight en React posiciona un único overlay de degradado radial en absoluto sobre el contenedor del grid y, después, escribe las coordenadas del cursor en propiedades CSS personalizadas mediante useMotionValue de Framer Motion. Cada tarjeta que queda debajo recibe el pase de iluminación cinematográfica sin ningún estado por tarjeta.
- Stack: React 18 + Framer Motion 11 + Tailwind v4 + Lucide React, ~100 líneas.
- Técnica de overlay único: una sola motion.div controla el spotlight de todo el grid, no una por tarjeta.
- Las propiedades CSS personalizadas (--mouse-x / --mouse-y) se enlazan directamente a los motion values, evitando re-renders en cada movimiento del ratón.
- Accesible: el contenido se lee sin el overlay; el spotlight es puramente decorativo y pointer-events:none.
- Advertencia móvil: no hay puntero en pantallas táctiles, así que el spotlight permanece oculto y el grid se muestra correctamente sin él.
Bento Spotlight Sweep es una sección de grid de funciones donde un único spotlight radial persigue al cursor por un layout de tarjetas multicolumna. Cuando el ratón recorre el grid, cada tarjeta por la que pasa se ilumina con un suave resplandor de acento, dando vida a la sección. El efecto se construye con un solo elemento overlay, sin estado por tarjeta, sin listeners de scroll, solo motion values de Framer Motion enlazados a propiedades CSS personalizadas.
Anatomía
El componente tiene tres capas. Primero, un bloque de cabecera opcional (badge, título, subtítulo) que aparece en fundido con una simple animación whileInView. Segundo, un contenedor de grid relative que captura los eventos mousemove y guarda una ref para el cálculo de coordenadas. Tercero, dentro de ese contenedor hay una única motion.div absolute, el overlay spotlight, dimensionada para cubrir todo el grid y controlada por las propiedades custom --mouse-x/--mouse-y. Las tarjetas en sí son elementos motion.div estándar que aparecen escalonados al hacer scroll; no necesitan ninguna lógica de spotlight especial.
Cómo funciona
El truco está en enlazar instancias useMotionValue de Framer Motion a propiedades CSS personalizadas en el elemento overlay. En cada mousemove, el handler lee la posición del puntero relativa al contenedor del grid mediante getBoundingClientRect y, después, llama a mouseX.set() y mouseY.set(). El overlay motion.div lee --mouse-x y --mouse-y dentro de una cadena de fondo radial-gradient. Como Framer Motion escribe los motion values directamente en el style del DOM sin disparar un re-render de React, la actualización del spotlight queda totalmente fuera del ciclo de React, fluida a 60fps incluso con muchas tarjetas.
Cómo crearlo en React
Configurar el contenedor del grid con una ref y un handler mousemove
Crea un div con position:relative, asígnale una ref y conecta un handler onMouseMove. Dentro del handler, resta el origen de getBoundingClientRect() del contenedor a los clientX/Y en bruto para obtener coordenadas relativas al contenedor.
const containerRef = React.useRef<HTMLDivElement>(null); const mouseX = useMotionValue(0); const mouseY = useMotionValue(0); function handleMouseMove(e: React.MouseEvent) { const rect = containerRef.current?.getBoundingClientRect(); if (!rect) return; mouseX.set(e.clientX - rect.left); mouseY.set(e.clientY - rect.top); }Colocar el overlay spotlight dentro del contenedor
Añade una motion.div como primer hijo del contenedor con position:absolute, inset-0, z-index por encima de las tarjetas y pointer-events:none. Enlaza los motion values a --mouse-x y --mouse-y mediante la prop style (Framer Motion acepta los motion values como propiedades CSS personalizadas).
<motion.div className="pointer-events-none absolute inset-0 z-10 rounded-2xl opacity-60" style={{ background: `radial-gradient(400px circle at var(--mouse-x) var(--mouse-y), color-mix(in srgb, var(--color-accent) 8%, transparent), transparent 60%)`, // @ts-expect-error CSS custom properties "--mouse-x": mouseX, "--mouse-y": mouseY, }} />Renderizar las tarjetas bento con un stagger whileInView
Recorre tu array de items y renderiza cada uno como una motion.div con un estado inicial opacity:0 / y:24, animándolo hasta visible al hacer scroll. Usa un pequeño multiplicador de delay (i * 0.08) para la cascada. El overlay spotlight se sitúa por encima de las tarjetas mediante z-index y las ilumina únicamente a través de la mezcla del degradado radial.
items.map((item, i) => ( <motion.div key={item.id} initial={{ opacity: 0, y: 24 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-40px" }} transition={{ delay: i * 0.08, duration: 0.5 }} className="relative rounded-2xl border p-6" /> ))Tematizar el color del spotlight con un token CSS
Reemplaza cualquier color fijo por var(--color-accent) dentro de la cadena radial-gradient. Ajusta el alpha mediante color-mix (8% funciona bien tanto en temas claros como oscuros). Para ensanchar o estrechar el haz, modifica el valor de radio 400px, los valores más grandes crean un resplandor más suave y ambiental.
Cuándo usarlo
Recurre a esta sección en páginas de funciones SaaS y portfolios de agencias donde quieras que el grid de funciones se sienta interactivo en lugar de estático. Funciona mejor en temas oscuros o atrevidos donde el resplandor de acento crea un contraste visible. Evítala en páginas con mucho texto donde la luz en movimiento compite con la lectura, y verifica siempre que el grid se vea bien sin el spotlight en móvil, el layout debe sostenerse por sí solo.
Usado por
- Vercel, Usa highlights radiales reactivos al cursor en sus secciones de funciones e infraestructura.
- Linear, Los efectos de resplandor spotlight en los grids de funciones dan al sitio del producto su aspecto pulido y distintivo.
- Resend, Layout de funciones estilo bento con efectos de luz ambiental que resaltan los bordes de las tarjetas al pasar el cursor.
FAQ
¿Por qué usar propiedades CSS personalizadas en lugar de un style inline con una template string?
Framer Motion puede escribir los motion values directamente en propiedades CSS personalizadas de un elemento del DOM, saltándose por completo el reconciliador de React. Esto significa que la cadena radial-gradient nunca es reevaluada por React en cada frame, el navegador simplemente lee los valores --mouse-x/--mouse-y actualizados, manteniendo la animación segura y sin tirones.
¿Funciona el spotlight cuando el grid se desplaza fuera de la pantalla?
El listener mousemove está en el propio contenedor del grid, así que solo se dispara cuando el cursor está dentro de los límites del grid. Cuando el usuario se desplaza a otro sitio, no se dispara ningún evento y el spotlight permanece en su última posición, invisible porque el contenedor está fuera del viewport.
¿Cómo ajusto el radio y la intensidad del spotlight?
Cambia el 400px de la cadena radial-gradient para hacer el círculo más grande o más pequeño. Sube o baja el porcentaje de color-mix (8% por defecto) para aumentar o reducir la intensidad del glow. Un valor entre 6% y 15% funciona bien; por encima del 20% el efecto empieza a verse duro en lugar de atmosférico.
¿Puedo añadir un highlight de borde por tarjeta además del spotlight global?
Sí. Sigue la posición del puntero relativa a cada tarjeta de forma individual (un segundo mousemove en cada div de tarjeta o un único listener que calcule los offsets por tarjeta), y luego define una propiedad CSS personalizada en esa tarjeta para controlar un border-image o un box-shadow. Esto combina bien con el overlay global para crear un efecto de profundidad por capas.