Retour au catalogue

Verify Email

Page de verification d'email avec code OTP a 6 chiffres.

authmedium Both Responsive a11y
minimalsaasuniversalcentered
Theme

Créer un champ OTP 6 chiffres en React

Un champ OTP React découpe un code en inputs contrôlés individuels, déplace le focus vers l'avant à la saisie d'un chiffre et vers l'arrière au Backspace, et valide chaque frappe contre un pattern numérique. Associe-le à une animation Framer Motion fade-up sur la carte pour terminer le tout en moins de 150 lignes.

  • Stack : React 18 + Framer Motion + Lucide React + Tailwind v4, ~87 lignes au total.
  • La gestion du focus utilise un tableau useRef, sans bibliothèque OTP tierce.
  • Chaque input rempli reçoit une bordure accent de 2px ; les inputs vides conservent une bordure neutre de 1px. L'état visuel est piloté uniquement par des custom properties CSS.
  • inputMode='numeric' déclenche le clavier numérique sur iOS et Android sans bloquer le collage.
  • La prop codeLength est configurable (défaut 6), donc les codes à 4 et 8 chiffres réutilisent le même composant sans modification.

AuthVerifyEmail est un écran de vérification d'email centré, construit autour d'un champ OTP multi-cases configurable. La carte entre en scène avec un fade-up spring, puis les cases gèrent toute la navigation clavier en interne pour que les développeurs l'intègrent sans recoder la logique de focus. C'est l'étape d'authentification la plus souvent négligée côté design.

Anatomie

L'écran est une seule motion.div centrée dans un conteneur min-h-screen. De haut en bas : un badge icône circulaire (Mail de Lucide, couleur accent), le titre h1, une ligne de sous-titre et d'email optionnelle, la rangée de chiffres OTP, un bouton de soumission pleine largeur, et un lien de renvoi. La rangée de chiffres parcourt un tableau de chaînes de longueur codeLength et affiche un input texte contrôlé par case.

Comment ça marche

L'état est un tableau de chaînes, une case par chiffre, toutes vides par défaut. handleChange filtre les entrées non numériques avec un test regex, écrit le dernier caractère tapé à l'index correspondant, puis déplace le focus vers l'input suivant via le tableau de refs. handleKeyDown intercepte le Backspace sur une case vide et recule le focus d'un cran. L'animation Framer Motion se joue une fois au montage : opacity 0 à 1, y 20 à 0, sur 600ms avec une courbe spring personnalisée.

Comment le coder en React

  1. Initialiser l'état des chiffres et les refs

    Crée un tableau de chaînes vides de longueur codeLength, puis un tableau useRef parallèle pour les inputs. Les refs permettent d'appeler .focus() de façon impérative sans gérer d'index de focus dans l'état.

    const [code, setCode] = useState<string[]>(
      Array.from({ length: codeLength }, () => "")
    );
    const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
  2. Gérer la saisie avec avance automatique

    À chaque événement change, rejette tout ce qui n'est pas un chiffre, ne retiens que le dernier caractère (gère le collage d'un seul chiffre), met à jour le tableau, puis donne le focus au slot suivant s'il existe.

    function handleChange(index: number, value: string) {
      if (!/^d*$/.test(value)) return;
      const newCode = [...code];
      newCode[index] = value.slice(-1);
      setCode(newCode);
      if (value && index < codeLength - 1) {
        inputRefs.current[index + 1]?.focus();
      }
    }
  3. Revenir en arrière au Backspace

    Quand la case courante est déjà vide et que l'utilisateur appuie sur Backspace, déplace le focus vers la case précédente. La correction devient naturelle sans suivi d'état supplémentaire.

    function handleKeyDown(index: number, e: React.KeyboardEvent) {
      if (e.key === "Backspace" && !code[index] && index > 0) {
        inputRefs.current[index - 1]?.focus();
      }
    }
  4. Animer l'entrée de la carte avec Framer Motion

    Entoure le conteneur d'une motion.div avec opacity 0 et y 20 en initial, anime vers opacity 1 et y 0, et passe un tableau ease spring. L'animation se joue une fois au montage et ne dépend pas de l'état interactif.

    const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];
    
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.6, ease }}
    >

Quand l'utiliser

Utilise ce composant partout où ton flux d'auth inclut une vérification par email ou téléphone : confirmation d'inscription, double facteur, ou code de réinitialisation de mot de passe. Il convient à l'onboarding SaaS, aux applis fintech, et à tout produit qui doit confirmer la possession d'un appareil ou d'une adresse. À éviter si ton flux utilise un magic link à la place d'un code ; le pattern multi-cases serait trompeur sans chiffres à saisir.

Utilisé par

  • Stripe, Utilise une rangée de cases OTP à 6 chiffres pour l'authentification à deux facteurs sur la connexion Dashboard.
  • GitHub, Affiche un champ OTP en cases séparées lors de la configuration 2FA et de la vérification d'appareil.
  • Linear, L'étape de vérification par email utilise un champ numérique segmenté cohérent avec son design system minimaliste.
  • Notion, La confirmation de connexion par email envoie un code à 6 chiffres saisi dans une interface à cases séparées.

FAQ

Comment gérer le collage d'un code complet ?

Ajoute un handler onPaste sur le premier input qui lit e.clipboardData.getData('text'), le découpe en caractères individuels, filtre les non-chiffres, remplit le tableau code, et met le focus sur la dernière case remplie. Le composant actuel gère le collage chiffre par chiffre ; le collage d'un code complet nécessite un handler supplémentaire.

Peut-on changer le nombre de chiffres sans forker le composant ?

Passe une prop codeLength différente ; le composant dérive à la fois le tableau d'état et les inputs rendus depuis cette valeur. Les codes SMS à 4 chiffres et les codes de secours à 8 chiffres fonctionnent sans autre modification.

inputMode='numeric' est-il suffisant pour les claviers mobiles ?

Sur la plupart des appareils oui : il affiche le clavier numérique sur iOS et Android sans bloquer le collage. Ajouter pattern='[0-9]*' en complément couvre les anciens WebViews Android qui ignorent inputMode.

Comment soumettre automatiquement quand le dernier chiffre est saisi ?

Dans handleChange, après avoir mis à jour le tableau code, vérifie si l'index mis à jour vaut codeLength - 1 et que la nouvelle valeur est non vide ; si c'est le cas, appelle ta fonction de soumission avec la chaîne code jointe. Un useEffect qui surveille le tableau code pour un état complet est tout aussi valable.

"use client";

import { useRef, useState } from "react";
import { motion } from "framer-motion";
import { Mail } from "lucide-react";

interface AuthVerifyEmailProps {
  brandName?: string;
  title?: string;
  subtitle?: string;
  email?: string;
  codeLength?: number;
}

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

export default function AuthVerifyEmail({
  brandName = "Acme",
  title = "Verifiez votre email",
  subtitle = "",
  email = "",
  codeLength = 6,

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Avis

Input OTP React, Code de vérification email 6 chiffres