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

Créer un formulaire d'authentification sans mot de passe en React

Un formulaire React sans mot de passe gère deux états visuels, saisie email et confirmation, échangés via AnimatePresence de Framer Motion en mode 'wait'. La phase d'envoi affiche une icône Sparkles en rotation pilotée par une boucle infinie, puis bascule vers un CheckCircle2 rebondissant une fois l'email envoyé.

  • Stack : React 18 + Framer Motion 11 + Lucide React, ~260 lignes, zéro dépendance supplémentaire.
  • Machine à états : trois phases gérées avec deux booléens, `sending` et `sent`, sans librairie de state externe.
  • Accessible : utilise un <form> natif avec type='email' et required, donc la validation navigateur et les lecteurs d'écran fonctionnent sans config.
  • Entièrement responsive avec une carte centrée de 400px max-width ; fonctionne sur toutes les tailles d'écran.
  • Toutes les couleurs utilisent des propriétés CSS custom (--color-accent, --color-background, etc.) pour s'adapter à n'importe quel preset de thème sans modifier le code.

AuthMagicLink est une section de connexion sans mot de passe conçue pour les applications React qui veulent supprimer les champs de mot de passe. Il gère le micro-parcours complet en un seul composant : saisie d'email, état de chargement animé et écran de succès avec option de réessai. La transition entre états est fluide et ressentie plutôt que mécanique, le genre de flow qui améliore la qualité perçue sans ajouter de complexité côté serveur.

Anatomie

La section occupe toute la hauteur de la fenêtre (min-height: 100vh) et centre une carte de 400px. Derrière la carte se trouve un halo radial flouté (opacité 0.04) qui réchauffe subtilement l'arrière-plan sans concurrencer le contenu. La carte elle-même a trois couches : un badge de marque en haut (icône Wand2 dans un carré coloré), un bloc titre/sous-titre, et la zone interactive. La zone interactive bascule entre le formulaire et le panneau de confirmation via AnimatePresence, en gardant les deux états au même emplacement DOM pour que le layout ne saute pas.

Comment ça marche

L'animation repose sur trois primitives Framer Motion. D'abord, la carte entière monte avec `initial={{ opacity: 0, y: 30 }}` déclenché par `useInView` avec une marge de -40px, pour qu'elle entre juste avant que l'utilisateur la scroll. Ensuite, l'icône de marque s'agrandit avec une ease de type spring séparée (`[0.16, 1, 0.3, 1]`). Enfin, l'échange formulaire/confirmation utilise `<AnimatePresence mode='wait'>` : l'élément sortant disparaît complètement avant que celui qui entre démarre, évitant tout chevauchement. Dans la confirmation, `animate={{ y: [0, -6, 0] }}` avec `repeat: Infinity` et `ease: 'easeInOut'` crée le CheckCircle2 flottant. L'état d'envoi pilote une rotation continue à 360° sur l'icône Sparkles via `transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}`.

Comment le coder en React

  1. Mettre en place la machine à états à trois phases

    Deux booléens couvrent les trois phases : idle (les deux à false), envoi (sending=true, sent=false) et confirmé (sent=true). Garde le gestionnaire de soumission du formulaire synchrone, appelle setSending(true) immédiatement, puis simule ou remplace par ton vrai appel API. Appelle setSent(true) dans le 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. Envelopper les deux états dans AnimatePresence

    Utilise `mode='wait'` pour que l'animation de sortie se termine avant que l'entrée démarre. Donne à chaque enfant une prop `key` stable, 'form' et 'sent', pour que Framer Motion sache quel élément entre et lequel sort. Sans clés distinctes, AnimatePresence ne peut pas détecter le changement.

    <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. Animer l'état d'envoi du bouton

    Dans le bouton de soumission, remplace le groupe d'icônes statiques par une icône en rotation quand `sending` est true. Une animation de rotation continue sur un motion.div enveloppant l'icône est lisible et signale l'activité sans composant spinner séparé. Désactive le bouton et mets le cursor à 'wait' pour bloquer l'interaction pendant la requête.

    {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. Ajouter l'icône de confirmation flottante

    Dans le panneau de confirmation, enveloppe le CheckCircle2 dans un motion.div avec une animation y en keyframes bouclant entre 0 et -6px. Utilise `ease: 'easeInOut'` et une durée de 2 secondes pour un flottement doux. Ce petit détail renforce l'état positif sans être distrayant.

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

Quand l'utiliser

Utilise ce composant sur tout produit SaaS ou outil supportant l'auth sans mot de passe par email (Auth.js, Supabase, Clerk, Firebase). Il fonctionne mieux comme page d'auth standalone, pas intégré dans une modal où la mise en page centrée entrerait en conflit avec le chrome de la modal. Évite-le si tu as besoin de boutons de connexion sociale ou d'un formulaire d'inscription multi-étapes ; cette mise en page est intentionnellement minimale et mono-objectif. Sur mobile il se rend correctement car aucune interaction ne dépend d'un pointeur.

Utilisé par

  • Linear, Utilise un flux de connexion par email uniquement comme chemin d'auth principal, cohérent avec leur philosophie produit minimaliste.
  • Notion, Propose en premier le lien magique envoyé par email avant d'afficher le mot de passe comme option secondaire.
  • Slack, Propose 'Se connecter avec un lien email' comme alternative aux mots de passe sur son écran de connexion.

FAQ

Comment connecter ce composant à un vrai backend de magic link ?

Remplace le `setTimeout` dans `handleSubmit` par ton vrai appel API, par exemple `supabase.auth.signInWithOtp({ email })` ou `fetch('/api/auth/magic-link', { method: 'POST', body: JSON.stringify({ email }) })`. Le composant ne gère que l'état visuel ; la couche réseau est branchée dans le handler.

Puis-je ajouter des boutons de connexion sociale à cette mise en page ?

Oui, étends le formulaire avec un séparateur et des boutons OAuth sous le champ email. Garde le max-width de la carte à 400px ; il gère confortablement 3 à 4 boutons sociaux avant de devenir visuellement chargé.

Pourquoi AnimatePresence mode='wait' plutôt que 'sync' ?

Avec 'sync', les animations de sortie et d'entrée se jouent simultanément, ce qui peut paraître chaotique quand les deux éléments s'agrandissent et disparaissent en même temps. 'wait' les enchaîne, le formulaire disparaît complètement avant que la confirmation entre, donnant à la transition un aspect plus propre et intentionnel.

Est-ce compatible avec les presets de thème de l'incubator ?

Directement. Toutes les couleurs référencent des propriétés CSS custom (`--color-accent`, `--color-background`, `--color-border`, etc.) définies par chaque preset. Change l'attribut `data-theme` sur un élément parent et le formulaire se repeint automatiquement.

"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

Avis

Formulaire de connexion sans mot de passe React, Code +