Retour au catalogue

Hero Particle Grid

Grille de points en arriere-plan. Les points proches du curseur grossissent et brillent. Layout split avec texte a gauche.

heromedium Both Responsive a11y
minimalboldsaassaassaassplit
Theme

Cómo crear un hero React con una grilla de puntos reactiva al ratón

Un hero React con grilla de puntos reactiva al cursor renderiza una grilla SVG de círculos y, en cada movimiento del puntero, muta directamente el radio y la opacidad de cada punto según su distancia euclidiana al cursor, sin re-renders, sin state. Los motion values de Framer Motion transmiten las coordenadas; un subscriber con useCallback las lee y llama a setAttribute sobre la ref del SVGCircleElement.

  • Stack: React 19 + Framer Motion 11 + lucide-react, ~190 líneas, sin dependencias adicionales.
  • 24 columnas × 14 filas = 336 puntos, con separación de 32px, centrados en la sección mediante transform SVG.
  • Rendimiento: cero re-renders de React al mover el ratón; las escrituras al DOM van directamente al elemento SVG mediante setAttribute.
  • Accesible: la grilla lleva aria-hidden, por lo que los lectores de pantalla la omiten por completo.
  • Salvedad táctil: sin puntero en móvil, los puntos se quedan en su opacidad de reposo (0.15) sin interacción.

Hero Particle Grid es un hero React con layout dividido donde una grilla de puntos SVG a sangre completa reacciona al puntero en tiempo real. Los puntos a menos de 120px del cursor crecen de 2px a 6px y se iluminan, creando un foco con profundidad de campo que sigue al ratón sin una sola actualización de state de React. El layout combina ese fondo animado con un titular alineado a la izquierda, una descripción y un CTA principal.

Anatomía

La sección es un contenedor flex a viewport completo (min-height 90vh) con DotGrid posicionado en absoluto detrás del contenido. DotGrid renderiza un elemento SVG (COLS × GAP de ancho, ROWS × GAP de alto) centrado con un transform CSS translate(-50%, -50%). Cada uno de los 336 puntos es un componente Dot que mantiene una ref a su SVGCircleElement. El bloque de texto vive en un contenedor con z-index:1 limitado a 600px, con una animación de entrada escalonada de Framer Motion (h1 → p → botones CTA, cada uno retrasado 0.1s).

Cómo funciona

Dos motion values de Framer Motion, mouseX y mouseY, se crean a nivel de DotGrid y se inicializan lejos fuera de pantalla (-1000, -1000). En cada mousemove, el handler lee el bounding rect del contenedor y escribe en ellas la posición del cursor relativa al contenedor. Cada Dot se suscribe a ambos motion values mediante la API .on('change', ...), cuando alguno se dispara, el punto calcula su distancia euclidiana al cursor y un factor de escala de 0 a 1 basado en un radio de 120px. Luego llama a ref.current.setAttribute para fijar r y opacity directamente en el elemento SVG, evitando por completo el reconciliador de React. Al salir el ratón, los valores vuelven a -1000 y cada punto regresa a su estado de reposo (r=2, opacity=0.15).

Cómo crearlo en React

  1. Construir la grilla de puntos SVG

    Define COLS, ROWS y GAP como constantes a nivel de módulo. En DotGrid, recorre cada combinación de fila/columna y agrega un Dot en la posición (col * GAP, row * GAP). Envuelve el SVG en un div con posición absolute con overflow:hidden y aria-hidden.

    const COLS = 24, ROWS = 14, GAP = 32, DOT_SIZE = 2;
    // inside DotGrid render:
    for (let row = 0; row < ROWS; row++)
      for (let col = 0; col < COLS; col++)
        dots.push(<Dot key={`${row}-${col}`} cx={col*GAP} cy={row*GAP} mouseX={mouseX} mouseY={mouseY} />);
  2. Conectar los motion values al puntero

    Crea mouseX y mouseY con useMotionValue(-1000) a nivel de DotGrid. Pásalos a cada Dot como props. En el handler onMouseMove, resta el getBoundingClientRect del contenedor para obtener coordenadas relativas al contenedor, luego llama a mouseX.set() y mouseY.set().

    const mouseX = useMotionValue(-1000);
    const mouseY = useMotionValue(-1000);
    
    const handleMouseMove = useCallback((e: React.MouseEvent) => {
      const rect = containerRef.current?.getBoundingClientRect();
      if (!rect) return;
      mouseX.set(e.clientX - rect.left);
      mouseY.set(e.clientY - rect.top);
    }, [mouseX, mouseY]);
  3. Suscribir cada punto y escribir directamente en el DOM

    Dentro del componente Dot, mantén una ref al SVGCircleElement. Registra un subscriber con mouseX.on('change', updateDot) y mouseY.on('change', updateDot). En updateDot, calcula la distancia, deriva r y opacity, y llama a setAttribute sobre la ref, sin setState, sin re-render.

    const updateDot = useCallback(() => {
      if (!ref.current) return;
      const dx = mouseX.get() - cx, dy = mouseY.get() - cy;
      const dist = Math.sqrt(dx*dx + dy*dy);
      const scale = Math.max(0, 1 - dist / 120);
      ref.current.setAttribute("r", String(DOT_SIZE + scale * 4));
      ref.current.setAttribute("opacity", String(0.15 + scale * 0.6));
    }, [cx, cy, mouseX, mouseY]);
    
    mouseX.on("change", updateDot);
    mouseY.on("change", updateDot);
  4. Animar el bloque de texto al montar

    Envuelve h1, p y la fila de CTA en elementos motion.* con initial={{ opacity: 0, y: 24/14/8 }} y animate={{ opacity: 1, y: 0 }}. Escalona los retrasos (0, 0.1, 0.2s) y usa la curva cubic-bezier [0.16, 1, 0.3, 1] para una sensación out-expo nítida que combina bien con la sutileza de la grilla.

    const EASE = [0.16, 1, 0.3, 1] as const;
    <motion.h1
      initial={{ opacity: 0, y: 24 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.7, ease: EASE }}
    >

Cuándo usarlo

Este hero funciona mejor en landing pages de SaaS o herramientas para desarrolladores donde buscas una estética técnica y precisa sin canvas pesado ni WebGL. La interacción transmite una sofisticación sutil más que espectáculo. Evítalo cuando tu objetivo por encima de la línea de flotación es la velocidad pura de conversión, los 336 elementos SVG añaden un pequeño coste de parsing, y prueba siempre en dispositivos Android de gama baja para confirmar que la interacción se mantiene fluida.

Usado por

  • Stripe, Usa fondos animados de grilla/puntos en sus páginas de producto para transmitir precisión técnica sin sobrecarga visual.
  • Resend, Adopta grillas de puntos dispersas y señales reactivas a la proximidad en secciones hero dirigidas a desarrolladores.
  • Clerk, Combina un texto hero alineado a la izquierda con fondos animados sutiles para lograr un tono limpio pero interactivo.
  • Raycast, Despliega capas de grano y partículas reactivas al puntero en su hero de landing para recompensar el movimiento exploratorio del ratón.

FAQ

¿Por qué suscribirse a los motion values en lugar de usar un state con onMouseMove?

Los motion values se disparan fuera del ciclo de render de React. Escribir las coordenadas en un useState provocaría 336 re-renders en cada evento del puntero, normalmente 60 veces por segundo. El enfoque del subscriber .on('change') escribe directamente en el DOM SVG, manteniendo el hilo principal libre y la animación fluida incluso a altas velocidades del cursor.

¿Cómo cambio el radio de influencia?

Edita la constante maxDist dentro del callback updateDot del Dot (actualmente 120px). Un valor mayor extiende el brillo sobre más puntos; uno menor crea un foco más ceñido y concentrado. La fórmula de escala `Math.max(0, 1 - dist / maxDist)` gestiona la caída automáticamente.

¿Puedo cambiar el color de los puntos según el tema?

Sí. Cada círculo usa fill="var(--color-accent)", así que basta con cambiar la custom property CSS en el elemento padre. El componente respeta los siete presets de tema del registry sin modificación.

¿Qué ocurre en pantallas táctiles?

Nada, los eventos táctiles no disparan mousemove, así que los motion values se quedan en su posición inicial -1000 y todos los puntos se renderizan en su estado de reposo (r=2, opacity=0.15). La sección sigue siendo totalmente utilizable, solo pierde la capa interactiva. Si quieres un fallback, detecta el táctil al montar y reemplaza la grilla por una versión estática.

"use client";

import { motion, useMotionValue } from "framer-motion";
import { ArrowRight } from "lucide-react";
import { useCallback, useRef } from "react";

interface HeroParticleGridProps {
  title?: string;
  titleAccent?: string;
  description?: string;
  ctaLabel?: string;
  ctaUrl?: string;
  ctaSecondaryLabel?: string;
}

const EASE = [0.16, 1, 0.3, 1] as const;
const COLS = 24;
const ROWS = 14;
const DOT_SIZE = 2;
const GAP = 32;

function DotGrid() {

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Reseñas

Hero React con grilla de puntos interactiva, tutorial Framer