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

Cómo crear una sección newsletter con spotlight radial que sigue el ratón en React

Una sección newsletter en React con spotlight de ratón sigue las coordenadas del puntero mediante useMotionValue y luego ensambla una cadena CSS radial-gradient dinámica con useMotionTemplate para que el resaltado siga al cursor en tiempo real. El formulario recorre los estados idle, loading (SVG giratorio) y success (checkmark animado), gestionados con useState y AnimatePresence.

  • Stack: React 18 + Framer Motion 11, ~376 líneas, cero dependencias de librería de iconos (solo SVG inline).
  • APIs principales de Framer Motion: useMotionValue, useMotionTemplate, AnimatePresence, motion.section, whileInView.
  • Accesible: el input tiene type=email + required, los SVG del spinner y del checkmark llevan aria-hidden, y el foco es visible mediante box-shadow.
  • Responsive: la fila del formulario salta de línea en anchos estrechos mediante flexWrap:wrap con un flex-basis en el input.
  • El spotlight no tiene efecto en dispositivos táctiles, el fondo de cuadrícula de puntos se sigue renderizando y la sección se lee con claridad sin interacción del cursor.

Esta sección newsletter convierte una captura de email estándar en un momento interactivo cuidado. Un spotlight radial persigue al ratón sobre un fondo de cuadrícula de puntos, el input de email se ilumina al enfocar y el botón de envío pasa de un loader giratorio a un checkmark confirmado, todo ello sin una sola librería de iconos externa ni CSS-in-JS. La fila de avatares apilados y el recuento de suscriptores cierran el bucle de prueba social.

Anatomía

La sección es un motion.section con un contenedor relative y overflow:hidden. Dos capas posicionadas en absoluto se sitúan bajo el contenido: un div de cuadrícula de puntos (aria-hidden, background-image radial-gradient con paso de 24px) y un motion.div para el overlay del spotlight. El contenido real, badge eyebrow, título en dos partes (normal + span con color accent), subtítulo, formulario y fila de prueba social, vive en un div centrado con z-index 1. La fila de prueba social apila tres círculos de color con margen negativo para simular avatares superpuestos.

Cómo funciona

En cada mousemove sobre la sección, el handler lee la posición del puntero relativa a la sección mediante getBoundingClientRect y escribe los valores en dos instancias useMotionValue de Framer Motion (mouseX, mouseY). useMotionTemplate ensambla luego esos valores dinámicos en una cadena CSS: `radial-gradient(500px circle at ${mouseX}px ${mouseY}px, color-mix(in srgb, var(--color-accent) 8%, transparent), transparent 70%)`. Esa cadena se vincula al estilo background del div overlay, de modo que el navegador repinta el degradado en cada frame sin re-renders de React. Los tres estados del formulario (idle, loading, success) se alternan con un useState simple; AnimatePresence en mode='wait' hace un cross-fade entre el formulario y la píldora de confirmación.

Cómo crearlo en React

  1. Conectar el spotlight de ratón con useMotionTemplate

    Crea dos motion values para las coordenadas del puntero y una ref para el elemento section. En el handler mousemove, resta el bounding rect de la sección para obtener coordenadas relativas al contenedor. Pasa mouseX y mouseY a useMotionTemplate para producir una cadena CSS de degradado dinámica y luego vincúlala al estilo background del 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. Construir la capa de fondo de cuadrícula de puntos

    Añade un div posicionado en absoluto con aria-hidden y un background-image radial-gradient con un tamaño de cuadrícula de 24px. Mantén la opacidad en torno a 0.35 para que se lea como textura y no como ruido. Coloca la motion.div del spotlight justo encima, también posicionada en absoluto con 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. Gestionar los estados del formulario con useState y AnimatePresence

    Declara un tipo FormState que cubra 'idle', 'loading' y 'success'. Al enviar, cambia a loading y simula una llamada async con setTimeout. Envuelve tanto el formulario como la píldora de éxito en AnimatePresence con mode='wait' para que uno se desvanezca antes de que el otro aparezca.

    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. Añadir la prueba social con avatares apilados

    Renderiza tres círculos pequeños en una fila flex, cada uno desplazado -8px hacia la izquierda mediante marginLeft en los índices > 0. Usa colores distintos dentro de la gama accent para dar variedad y un borde de 2px que coincida con el color de fondo para crear la ilusión de separación. Haz seguir la fila con una cadena de recuento de suscriptores.

    {["#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,
        }}
      />
    ))}

Cuándo usarlo

Usa esta sección en sitios SaaS, de agencia o de marca de contenido donde la captura de email sea un objetivo de conversión principal. Encaja a mitad de página tras un bloque de features o testimonios, y funciona bien justo encima del footer. Evítala cuando la página ya tiene un hero interactivo pesado, dos superficies reactivas al cursor en la misma página compiten por la atención. En páginas puramente transaccionales (checkout, onboarding), un formulario inline más sencillo distrae menos.

Usado por

  • Linear, Usa overlays de spotlight radial en secciones oscuras para atraer la atención sin añadir ruido visual.
  • Vercel, Emplea degradados ambientales guiados por el cursor en secciones de marketing, una técnica cercana a este patrón.
  • Superhuman, Combina una captura de email centrada y limpia con una animación de fondo sutil y recuentos de suscriptores como prueba social.
  • Beehiiv, Las landing pages de newsletter combinan habitualmente recuentos de suscriptores, avatares apilados y un único campo de email para secciones de suscripción de alta conversión.

FAQ

¿Por qué usar useMotionTemplate en lugar de una variable CSS normal?

useMotionTemplate vincula un MotionValue de Framer Motion directamente en una cadena CSS para que el navegador actualice el estilo en cada frame de animación sin pasar por el ciclo de render de React. Una actualización de state normal en mousemove provocaría re-renders constantes y resultaría entrecortada a altas velocidades de puntero.

¿El spotlight sigue funcionando en modo oscuro?

Sí. El degradado usa color-mix(in srgb, var(--color-accent) 8%, transparent), así que toma el color accent que defina el tema activo. El themeMode del componente está fijado en 'both', lo que significa que se adapta a los siete presets sin cambios de código.

¿Cómo conecto el formulario a un servicio de email real?

Reemplaza el mock de setTimeout en handleSubmit por una llamada fetch a tu ruta API (Mailchimp, ConvertKit, Resend, etc.). Mantén el patrón formState tal cual: pon 'loading' antes del await y 'success' (o un estado de error) después. La transición de AnimatePresence funciona igual sea cual sea la fuente async.

El spotlight se ve demasiado sutil. ¿Cómo lo intensifico?

Aumenta el porcentaje de opacidad en color-mix, de 8% hasta 15-20%, y amplía el radio del círculo de 500px a 700px. Por encima del 25% el efecto empieza a competir con el contraste del texto, así que pruébalo en todos los presets de tema antes de confirmar.

"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

Reseñas

Sección newsletter en React con spotlight de ratón, Tutorial