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

Créer un hero React avec une grille de points réactive à la souris

Un hero React à grille de points réactive au curseur rend une grille SVG de cercles et, à chaque déplacement du pointeur, mute directement le rayon et l'opacité de chaque point selon sa distance euclidienne au curseur, sans re-render, sans state. Les motion values Framer Motion transmettent les coordonnées ; un subscriber useCallback les lit et appelle setAttribute sur la ref SVGCircleElement.

  • Stack : React 19 + Framer Motion 11 + lucide-react, ~190 lignes, aucune dépendance supplémentaire.
  • 24 colonnes × 14 lignes = 336 points, espacement 32px, centrés dans la section via transform SVG.
  • Performance : zéro re-render React au déplacement de la souris ; les écritures DOM vont directement sur l'élément SVG via setAttribute.
  • Accessible : la grille porte aria-hidden, les lecteurs d'écran la sautent complètement.
  • Mobile : sans pointeur, les points restent à leur opacité de repos (0.15) sans interaction.

Hero Particle Grid est un hero React en layout split où une grille SVG de points réagit au pointeur en temps réel. Les points à moins de 120px du curseur grossissent de 2px à 6px et s'illuminent, créant un spotlight à profondeur de champ qui suit la souris sans le moindre update de state React. Le layout associe ce fond animé à un titre aligné à gauche, une description et un CTA principal.

Anatomie

La section est un conteneur flex plein viewport (min-height 90vh) avec DotGrid positionné en absolu derrière le contenu. DotGrid rend un élément SVG (COLS × GAP de large, ROWS × GAP de haut) centré par un transform CSS translate(-50%, -50%). Chacun des 336 points est un composant Dot qui tient une ref vers son SVGCircleElement. Le bloc texte est dans un conteneur z-index:1 limité à 600px, avec une animation d'entrée Framer Motion décalée (h1 → p → boutons CTA, chacun retardé de 0.1s).

Comment ça marche

Deux motion values Framer Motion, mouseX et mouseY, sont créées au niveau de DotGrid et initialisées loin hors écran (-1000, -1000). À chaque mousemove, le handler lit le bounding rect du conteneur et écrit la position relative au curseur dedans. Chaque Dot s'abonne aux deux motion values via l'API .on('change', ...), quand l'une déclenche, le dot calcule sa distance euclidienne au curseur et un facteur d'échelle de 0 à 1 sur un rayon de 120px. Il appelle ensuite ref.current.setAttribute pour définir r et opacity directement sur l'élément SVG, court-circuitant complètement le réconciliateur React. Au mouse leave, les valeurs reviennent à -1000 et chaque point retombe à son état de repos (r=2, opacity=0.15).

Comment le coder en React

  1. Construire la grille SVG de points

    Définis COLS, ROWS et GAP en constantes au niveau du module. Dans DotGrid, boucle sur chaque combinaison ligne/col et pousse un Dot à la position (col * GAP, row * GAP). Enveloppe le SVG dans un div en position absolute avec overflow:hidden et 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. Brancher les motion values sur le pointeur

    Crée mouseX et mouseY avec useMotionValue(-1000) au niveau de DotGrid. Passe-les à chaque Dot en props. Dans le handler onMouseMove, soustrait le getBoundingClientRect du conteneur pour obtenir des coordonnées relatives, puis appelle mouseX.set() et 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. Abonner chaque point et écrire directement dans le DOM

    Dans le composant Dot, tiens une ref sur le SVGCircleElement. Enregistre un subscriber avec mouseX.on('change', updateDot) et mouseY.on('change', updateDot). Dans updateDot, calcule la distance, dérive r et opacity, et appelle setAttribute sur la ref, pas de setState, pas de 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. Animer le bloc texte à l'apparition

    Enveloppe h1, p et la rangée de CTA dans des motion.* avec initial={{ opacity: 0, y: 24/14/8 }} et animate={{ opacity: 1, y: 0 }}. Décale les délais (0, 0.1, 0.2s) et utilise la courbe de Bézier [0.16, 1, 0.3, 1] pour un effet out-expo qui s'harmonise avec la subtilité de la grille.

    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 }}
    >

Quand l'utiliser

Ce hero convient aux landing pages SaaS ou outils développeurs où tu veux une esthétique technique et précise sans canvas lourd ni WebGL. L'interaction donne une impression de sophistication subtile plutôt que de spectacle. Évite-le quand ton objectif au-dessus de la ligne de flottaison est la conversion pure, 336 éléments SVG ajoutent un petit coût de parsing, et teste toujours sur Android entrée de gamme pour confirmer la fluidité.

Utilisé par

  • Stripe, Utilise des fonds animés à grille/points sur ses pages produit pour exprimer la précision technique sans surcharge visuelle.
  • Resend, Adopte des grilles de points clairsemées et des indices de proximité dans ses heros ciblant les développeurs.
  • Clerk, Combine un texte hero aligné à gauche avec des fonds animés subtils pour un ton épuré mais interactif.
  • Raycast, Déploie des couches de grain et particules réactives au pointeur sur son hero landing pour récompenser les déplacements de souris exploratoires.

FAQ

Pourquoi s'abonner aux motion values plutôt qu'utiliser un state onMouseMove ?

Les motion values se déclenchent en dehors du cycle de rendu React. Écrire les coordonnées dans un useState provoquerait 336 re-renders à chaque événement pointeur, soit environ 60 fois par seconde. L'approche .on('change') écrit directement dans le DOM SVG, libérant le thread principal et maintenant l'animation fluide même à haute vitesse de curseur.

Comment modifier le rayon d'influence ?

Modifie la constante maxDist dans le callback updateDot du Dot (actuellement 120px). Une valeur plus grande étend la lueur sur plus de points ; une plus petite crée un spotlight plus serré. La formule `Math.max(0, 1 - dist / maxDist)` gère la décroissance automatiquement.

Peut-on changer la couleur des points selon le thème ?

Oui. Chaque cercle utilise fill="var(--color-accent)", donc il suffit de changer la custom property CSS sur l'élément parent. Le composant respecte les sept presets de thème du registry sans modification.

Que se passe-t-il sur écrans tactiles ?

Rien, les événements tactiles ne déclenchent pas mousemove, donc les motion values restent à leur position initiale -1000 et tous les points s'affichent à leur état de repos (r=2, opacity=0.15). La section reste entièrement utilisable, elle perd simplement la couche interactive. Pour un fallback, détecte le touch au montage et remplace la grille par une version statique.

"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

Avis

Hero React grille de points interactive, Tutoriel Framer