Retour au catalogue

Newsletter Spotlight

Section newsletter avec spotlight radial qui suit la souris via useMotionTemplate. Dot-grid subtil en fond, glow sur l'input au focus, bouton avec spinner SVG anime puis checkmark au succes. Social proof avec avatars empiles.

newslettermedium Both Responsive a11y
minimalelegantboldsaasuniversalagencycentered
Theme

Créer une section newsletter React avec spotlight radial qui suit la souris

Une section newsletter React avec spotlight souris suit les coordonnées du pointeur via useMotionValue, puis assemble une chaîne CSS radial-gradient dynamique avec useMotionTemplate pour que le halo suive le curseur en temps réel. Le formulaire passe par trois états, idle, loading (spinner SVG) et success (checkmark animé), gérés avec useState et AnimatePresence.

  • Stack : React 18 + Framer Motion 11, ~376 lignes, aucune dépendance d'icônes (SVG inline uniquement).
  • APIs Framer Motion clés : useMotionValue, useMotionTemplate, AnimatePresence, motion.section, whileInView.
  • Accessible : l'input a type=email + required, les SVGs spinner et checkmark portent aria-hidden, et le focus est visible via box-shadow.
  • Responsive : la rangée du formulaire passe en colonne sur mobile via flexWrap:wrap et un flex-basis sur l'input.
  • Le spotlight n'a aucun effet sur mobile, la grille de points reste visible et la section reste lisible sans interaction curseur.

Cette section newsletter transforme une capture d'email standard en moment interactif soigné. Un spotlight radial suit la souris sur un fond à grille de points, l'input s'illumine au focus, et le bouton passe d'un spinner en rotation à un checkmark de confirmation, sans aucune bibliothèque d'icônes ni CSS-in-JS. La rangée d'avatars empilés et le compte d'abonnés clôturent la boucle de preuve sociale.

Anatomie

La section est une motion.section avec un conteneur relative en overflow:hidden. Deux couches positionnées en absolu se trouvent sous le contenu : un div grille de points (aria-hidden, background-image radial-gradient en pas de 24px) et une motion.div pour l'overlay spotlight. Le contenu réel, badge eyebrow, titre en deux parties (normal + span coloré accent), sous-titre, formulaire et rangée social proof, est dans un div centré à z-index 1. La rangée social proof empile trois cercles colorés avec marge négative pour simuler des avatars superposés.

Comment ça marche

À chaque mousemove sur la section, le handler lit la position du pointeur relative à la section via getBoundingClientRect et écrit les valeurs dans deux useMotionValue Framer Motion (mouseX, mouseY). useMotionTemplate assemble ces valeurs dynamiques en chaîne CSS : `radial-gradient(500px circle at ${mouseX}px ${mouseY}px, color-mix(in srgb, var(--color-accent) 8%, transparent), transparent 70%)`. Cette chaîne est liée au style background de la div overlay, donc le navigateur repeint le dégradé à chaque frame sans re-render React. Les trois états du formulaire (idle, loading, success) sont basculés avec un useState simple ; AnimatePresence en mode='wait' effectue un cross-fade entre le formulaire et la pilule de confirmation.

Comment le coder en React

  1. Connecter le spotlight souris avec useMotionTemplate

    Crée deux motion values pour les coordonnées du pointeur et une ref pour l'élément section. Dans le handler mousemove, soustrait le bounding rect de la section pour obtenir des coordonnées relatives au conteneur. Passe mouseX et mouseY dans useMotionTemplate pour produire une chaîne CSS dynamique, puis lie-la au style background de la div overlay.

    const mouseX = useMotionValue(0);
    const mouseY = useMotionValue(0);
    const sectionRef = useRef<HTMLElement>(null);
    
    const spotlightBg = useMotionTemplate`radial-gradient(
      500px circle at ${mouseX}px ${mouseY}px,
      color-mix(in srgb, var(--color-accent) 8%, transparent),
      transparent 70%
    )`;
    
    function handleMouseMove(e: React.MouseEvent<HTMLElement>) {
      const rect = sectionRef.current?.getBoundingClientRect();
      if (!rect) return;
      mouseX.set(e.clientX - rect.left);
      mouseY.set(e.clientY - rect.top);
    }
  2. Construire la couche de fond à grille de points

    Ajoute un div positionné en absolu avec aria-hidden et un background-image radial-gradient à un pas de grille de 24px. Garde une opacité d'environ 0.35 pour qu'il lise comme une texture plutôt que du bruit. Place la motion.div spotlight directement par-dessus, également positionnée en absolu avec inset:0.

    <div
      aria-hidden
      style={{
        position: "absolute",
        inset: 0,
        backgroundImage:
          "radial-gradient(circle, color-mix(in srgb, var(--color-foreground) 10%, transparent) 1px, transparent 1px)",
        backgroundSize: "24px 24px",
        opacity: 0.35,
        pointerEvents: "none",
      }}
    />
    <motion.div
      aria-hidden
      style={{ position: "absolute", inset: 0, background: spotlightBg, pointerEvents: "none" }}
    />
  3. Gérer les états du formulaire avec useState et AnimatePresence

    Déclare un type FormState couvrant 'idle', 'loading' et 'success'. À la soumission, passe en loading et simule un appel async avec setTimeout. Enveloppe le formulaire et la pilule de succès dans AnimatePresence en mode='wait' pour que l'un fade out avant que l'autre fade in.

    type FormState = "idle" | "loading" | "success";
    const [formState, setFormState] = useState<FormState>("idle");
    
    function handleSubmit(e: React.FormEvent) {
      e.preventDefault();
      if (formState !== "idle") return;
      setFormState("loading");
      setTimeout(() => setFormState("success"), 1500);
    }
    
    <AnimatePresence mode="wait">
      {formState === "success" ? (
        <motion.div key="success" initial={{ opacity: 0, scale: 0.92 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0 }}>
          <CheckIcon /> Vous êtes inscrit !
        </motion.div>
      ) : (
        <motion.form key="form" onSubmit={handleSubmit} exit={{ opacity: 0 }}>
          {/* input + button */}
        </motion.form>
      )}
    </AnimatePresence>
  4. Ajouter la rangée social proof avec avatars empilés

    Rends trois petits cercles dans une rangée flex, chacun décalé de -8px vers la gauche via marginLeft sur les indices > 0. Utilise des couleurs distinctes dans la gamme accent pour la variété et une bordure de 2px correspondant à la couleur de fond pour créer l'illusion de séparation. Fais suivre la rangée par une chaîne de décompte d'abonnés.

    {["#818cf8", "#a78bfa", "#60a5fa"].map((bg, i) => (
      <div
        key={i}
        style={{
          width: 24,
          height: 24,
          borderRadius: "50%",
          border: "2px solid var(--color-background)",
          background: bg,
          marginLeft: i === 0 ? 0 : -8,
        }}
      />
    ))}

Quand l'utiliser

Utilise cette section sur des sites SaaS, d'agence ou de marque de contenu où la capture d'email est un objectif de conversion principal. Elle convient au milieu de page après un bloc features ou témoignages, et fonctionne bien juste avant le footer. Évite-la quand la page a déjà un hero interactif lourd, deux surfaces réactives au curseur sur la même page se disputent l'attention. Sur les pages purement transactionnelles (checkout, onboarding), un formulaire inline plus simple est moins distrayant.

Utilisé par

  • Linear, Utilise des overlays spotlight radiaux sur les sections sombres pour attirer l'attention sans ajouter de bruit visuel.
  • Vercel, Emploie des dégradés ambiants pilotés par le curseur sur les sections marketing, une technique proche de ce pattern.
  • Superhuman, Associe une capture email centrée épurée à une animation de fond subtile et un compteur d'abonnés en preuve sociale.
  • Beehiiv, Les landing pages de newsletter combinent régulièrement un compteur d'abonnés, des avatars empilés et un seul champ email pour des sections d'inscription à fort taux de conversion.

FAQ

Pourquoi utiliser useMotionTemplate plutôt qu'une variable CSS classique ?

useMotionTemplate lie une MotionValue Framer Motion directement à une chaîne CSS pour que le navigateur mette à jour le style à chaque frame sans passer par le cycle de rendu React. Une mise à jour de state classique au mousemove déclencherait des re-renders constants et serait saccadée à haute vitesse de pointeur.

Le spotlight fonctionne-t-il toujours en mode sombre ?

Oui. Le dégradé utilise color-mix(in srgb, var(--color-accent) 8%, transparent), donc il reprend la couleur accent définie par le thème actif. Le themeMode du composant est réglé sur 'both', ce qui signifie qu'il s'adapte aux sept presets sans modification de code.

Comment connecter le formulaire à un vrai service d'emailing ?

Remplace le mock setTimeout dans handleSubmit par un appel fetch vers ta route API (Mailchimp, ConvertKit, Resend, etc.). Garde le pattern formState tel quel : passe à 'loading' avant l'await et à 'success' (ou un état d'erreur) après. La transition AnimatePresence fonctionne de la même façon quelle que soit la source async.

Le spotlight est trop subtil. Comment l'intensifier ?

Augmente le pourcentage d'opacité dans color-mix, de 8% à 15-20%, et élargis le rayon du cercle de 500px à 700px. Au-delà de 25%, l'effet commence à concurrencer le contraste du texte, donc teste sur tous les presets de thème avant de valider.

"use client";

import { useState, useRef } from "react";
import {
  motion,
  useMotionValue,
  useMotionTemplate,
  AnimatePresence,
} from "framer-motion";

interface NewsletterSpotlightProps {
  title?: string;
  titleAccent?: string;
  subtitle?: string;
  placeholder?: string;
  ctaLabel?: string;
  socialProof?: string;
}

const EASE: [number, number, number, number] = [0.16, 1, 0.3, 1];

function SpinnerIcon() {

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Avis

Section newsletter React avec spotlight souris, Tutoriel