Retour au catalogue

Magic Link

Connexion sans mot de passe par magic link avec animation d'envoi et confirmation visuelle.

authsimple Both Responsive a11y
minimalelegantsaasuniversalcentered
Theme

Cómo crear un formulario de autenticación por email sin contraseña en React

Un formulario de autenticación en React sin contraseña gestiona dos estados visuales, un campo de email y una pantalla de confirmación, intercambiados con AnimatePresence de Framer Motion en modo 'wait'. La fase de envío muestra un icono Sparkles que gira mediante un bucle de motion infinito, y luego cambia a un CheckCircle2 que rebota una vez enviado.

  • Stack: React 18 + Framer Motion 11 + Lucide React, ~260 líneas, cero dependencias adicionales.
  • Máquina de estados: tres fases gestionadas con dos booleanos, `sending` y `sent`, sin necesidad de librería de state externa.
  • Accesible: usa un <form> nativo con type='email' y required, por lo que la validación del navegador y los lectores de pantalla funcionan sin configuración.
  • Totalmente responsive con una tarjeta centrada de 400px de max-width; funciona en cualquier tamaño de pantalla.
  • Todos los colores usan propiedades CSS custom (--color-accent, --color-background, etc.) para adaptarse a cualquier preset de tema sin cambiar el código.

AuthMagicLink es una sección de login sin contraseña creada para aplicaciones React que quieren eliminar por completo los campos de contraseña. Gestiona todo el micro-recorrido en un solo componente: captura del email, un estado de carga animado y una pantalla de éxito con opción de reintento. La transición entre estados es fluida y se siente natural en lugar de mecánica, el tipo de flujo que aumenta la calidad percibida sin añadir complejidad en el backend.

Anatomía

La sección ocupa todo el viewport (min-height: 100vh) y centra una única tarjeta de 400px. Detrás de la tarjeta hay un resplandor radial difuminado (opacidad 0.04) que da calidez sutilmente al fondo sin competir con el contenido. La tarjeta en sí tiene tres capas: un badge de marca arriba (icono Wand2 en un cuadrado con color de acento), un bloque de título/subtítulo y la zona interactiva. La zona interactiva alterna entre el formulario y el panel de confirmación mediante AnimatePresence, manteniendo ambos estados en la misma posición del DOM para que el layout nunca salte.

Cómo funciona

La animación se apoya en tres primitivas de Framer Motion. Primero, toda la tarjeta se monta con `initial={{ opacity: 0, y: 30 }}` disparado por `useInView` con un margen de -40px, de modo que entra justo antes de que el usuario llegue a ella al hacer scroll. Segundo, el icono de marca escala con una ease propia de tipo spring (`[0.16, 1, 0.3, 1]`). Tercero, el intercambio formulario/confirmación usa `<AnimatePresence mode='wait'>`: el elemento saliente se desvanece y escala completamente antes de que el entrante empiece, evitando el solapamiento. Dentro de la confirmación, `animate={{ y: [0, -6, 0] }}` con `repeat: Infinity` y `ease: 'easeInOut'` crea el CheckCircle2 flotante. El estado de envío impulsa una rotación continua de 360 grados sobre el icono Sparkles mediante `transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}`.

Cómo crearlo en React

  1. Montar la máquina de estados de tres fases

    Dos booleanos cubren las tres fases: idle (ambos en false), envío (sending=true, sent=false) y confirmado (sent=true). Mantén el handler de submit del formulario síncrono, llama a setSending(true) de inmediato y luego simula o reemplaza por tu llamada real a la API. Llama a setSent(true) en el callback.

    const [sent, setSent] = useState(false);
    const [sending, setSending] = useState(false);
    
    const handleSubmit = (e: React.FormEvent) => {
      e.preventDefault();
      setSending(true);
      sendMagicLink(email).then(() => {
        setSending(false);
        setSent(true);
      });
    };
  2. Envolver ambos estados en AnimatePresence

    Usa `mode='wait'` para que la animación de salida termine antes de que empiece la de entrada. Dale a cada hijo una prop `key` estable, 'form' y 'sent', para que Framer Motion sepa qué elemento entra y cuál sale. Sin keys distintas, AnimatePresence no puede detectar el cambio.

    <AnimatePresence mode="wait">
      {sent ? (
        <motion.div key="sent"
          initial={{ opacity: 0, scale: 0.9 }}
          animate={{ opacity: 1, scale: 1 }}
          exit={{ opacity: 0, scale: 0.9 }}
          transition={{ duration: 0.5 }}
        >
          {/* confirmation content */}
        </motion.div>
      ) : (
        <motion.form key="form"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          onSubmit={handleSubmit}
        >
          {/* email input + button */}
        </motion.form>
      )}
    </AnimatePresence>
  3. Animar el estado de envío del botón

    Dentro del botón de submit, reemplaza el grupo de iconos estático por un icono que gira cuando `sending` es true. Una animación de rotación continua sobre un motion.div que envuelve el icono se ve limpia y señala actividad sin un componente spinner aparte. Desactiva el botón y pon el cursor en 'wait' para bloquear la interacción mientras la petición está en curso.

    {sending ? (
      <motion.div
        animate={{ rotate: 360 }}
        transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
      >
        <Sparkles style={{ width: 18, height: 18 }} />
      </motion.div>
    ) : (
      <>
        <Sparkles style={{ width: 16, height: 16 }} />
        {ctaLabel}
        <ArrowRight style={{ width: 14, height: 14 }} />
      </>
    )}
  4. Añadir el icono de confirmación flotante

    En el panel de confirmación, envuelve el CheckCircle2 en un motion.div con una animación de y en keyframes que oscila entre 0 y -6px. Usa `ease: 'easeInOut'` y una duración de 2 segundos para una flotación suave. Este pequeño detalle refuerza el estado positivo sin resultar molesto.

    <motion.div
      animate={{ y: [0, -6, 0] }}
      transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
    >
      <CheckCircle2 style={{ width: 30, height: 30, color: "var(--color-accent)" }} />
    </motion.div>

Cuándo usarlo

Usa este componente en cualquier producto SaaS o herramienta que admita auth sin contraseña por email (Auth.js, Supabase, Clerk, Firebase). Funciona mejor como página de auth independiente, no embebida en una modal, donde el layout centrado entraría en conflicto con el chrome de la modal. Evítalo si necesitas botones de login social o un formulario de registro de varios pasos; este layout es intencionadamente minimalista y de un solo propósito. En móvil se renderiza correctamente, ya que no hay ninguna interacción que dependa de un puntero.

Usado por

  • Linear, Usa un flujo de login solo por email como ruta de auth principal, en línea con su filosofía de producto minimalista.
  • Notion, Por defecto envía un magic link al email antes de mostrar la contraseña como opción secundaria.
  • Slack, Ofrece 'Iniciar sesión con enlace por email' como alternativa a las contraseñas en su pantalla de login.

FAQ

¿Cómo conecto este componente a un backend real de magic link?

Reemplaza el `setTimeout` en `handleSubmit` por tu llamada real a la API, por ejemplo `supabase.auth.signInWithOtp({ email })` o `fetch('/api/auth/magic-link', { method: 'POST', body: JSON.stringify({ email }) })`. El componente solo controla el estado visual; la capa de red se conecta en el handler.

¿Puedo añadir botones de login social a este layout?

Sí, amplía el formulario con un separador y botones OAuth debajo del campo de email. Mantén el max-width de la tarjeta en 400px; admite cómodamente hasta 3 o 4 botones sociales antes de verse recargado.

¿Por qué AnimatePresence mode='wait' en lugar de 'sync'?

Con 'sync', las animaciones de salida y entrada se ejecutan a la vez, lo que puede verse caótico cuando ambos elementos escalan y se desvanecen al mismo tiempo. 'wait' las secuencia, el formulario desaparece por completo antes de que la confirmación aparezca, dando a la transición una sensación más limpia e intencionada.

¿Funciona con los presets de tema de incubator?

Directamente. Todos los colores referencian propiedades CSS custom (`--color-accent`, `--color-background`, `--color-border`, etc.) definidas por cada preset. Cambia el atributo `data-theme` en un elemento padre y el formulario se repinta automáticamente.

"use client";

import { useState, useRef } from "react";
import { motion, useInView, AnimatePresence } from "framer-motion";
import { Sparkles, Mail, ArrowRight, CheckCircle2, Wand2 } from "lucide-react";

interface AuthMagicLinkProps {
  brandName?: string;
  title?: string;
  subtitle?: string;
  placeholder?: string;
  ctaLabel?: string;
  sentTitle?: string;
  sentMessage?: string;
  retryLabel?: string;
  footerNote?: string;
}

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

export default function AuthMagicLink({
  brandName = "Flux",

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Reseñas

Formulario de login sin contraseña en React (Magic Link),