Saisie de code (OTP)

Champ de saisie pour code de validation à usage unique (One-Time Password), intégrant la gestion du copier-coller et l'autofill SMS.

1. Démonstration

Code de vérification (6 chiffres)

2. Accessibilité et UX

Ce composant combine plusieurs techniques pour garantir une saisie fluide :

Attribut / Technique Explication
autocomplete="one-time-code" Permet aux smartphones de proposer automatiquement le code reçu par SMS. Il ne se place que sur le premier champ.
inputmode="numeric" + pattern="[0-9]" Force l'affichage du pavé numérique sur mobile et empêche la validation si la saisie n'est pas un chiffre de 0 à 9.
Label vs Tooltip Chaque champ possède un <label class="sr-only"> lu par les lecteurs d'écran. Les tooltips visuels, eux, sont masqués aux lecteurs d'écran via aria-hidden="true" pour éviter les répétitions vocales.

3. Code CSS (Adapté au thème minimaliste)

Ce CSS utilise les variables du système pour s'adapter dynamiquement au mode clair/sombre, avec des contours francs (sans bordures arrondies) et une inversion de couleur sur les tooltips.

.otp-group {
  border: none;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.otp-inputs {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.input-wrapper {
  position: relative;
  display: inline-block;
}

/* Tooltip visuel inversé (Fond texte, Texte fond) */
.tooltip {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  background-color: var(--text-color);
  color: var(--bg-color);
  padding: 4px 8px;
  margin-bottom: 8px;
  font-size: 0.85rem;
  font-weight: 700;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition-speed) ease;
  z-index: 10;
}

/* Afficher le tooltip au focus ou survol */
.input-wrapper:focus-within .tooltip,
.input-wrapper:hover .tooltip {
  opacity: 1;
  pointer-events: auto;
}

/* Les cases de saisie */
.otp-inputs input {
  aspect-ratio: 3/4;
  width: 44px;
  text-align: center;
  font-size: 1.5rem;
  font-weight: 700;
  border: 2px solid var(--border-color);
  background-color: var(--panel-bg);
  color: var(--text-color);
  border-radius: 0; /* Minimalisme : on retire le border-radius de 8px */
  transition: background-color var(--transition-speed), color var(--transition-speed);
}

.otp-inputs input:focus-visible {
  outline: 4px solid var(--text-color);
  outline-offset: 4px;
}

4. Code JavaScript (Le moteur)

Un champ OTP multiple nécessite impérativement du JavaScript pour gérer les déplacements de curseur et l'interception des copier-coller.

Note de compatibilité : La boucle setInterval en bas du script est un correctif (polyfill) indispensable. Sur Safari iOS, le remplissage automatique par SMS met parfois à jour la valeur des champs silencieusement, sans déclencher l'événement Javascript standard "input".

<script>
  const inputs = Array.from(document.querySelectorAll('#codeForm input[type="text"]'));

  function setCodeToInputs(code, startIndex = 0) {
    for (let i = 0; i < code.length && (startIndex + i) < inputs.length; i++) {
      inputs[startIndex + i].value = code[i];
    }
    const nextIndex = startIndex + code.length;
    if (nextIndex < inputs.length) {
      inputs[nextIndex].focus();
    } else {
      inputs[inputs.length - 1].focus();
    }
  }

  inputs.forEach((input, index) => {
    input.addEventListener('input', e => {
      let value = e.target.value.replace(/[^0-9]/g, '');

      if (value.length > 1) {
        // Collage ou autofill : répartir les chiffres
        setCodeToInputs(value, index);
      } else {
        e.target.value = value.slice(0, 1);
        if (value && index < inputs.length - 1) {
          inputs[index + 1].focus();
        }
      }
    });

    input.addEventListener('keydown', e => {
      // Retour arrière fluide vers les cases précédentes
      if (e.key === 'Backspace' && input.value === '' && index > 0) {
        inputs[index - 1].focus();
      }
    });
  });

  // Interception du collage (Paste) sur le premier input
  inputs[0].addEventListener('paste', e => {
    e.preventDefault();
    const paste = (e.clipboardData || window.clipboardData).getData('text');
    const digits = paste.replace(/[^0-9]/g, '').slice(0, inputs.length);
    if (digits.length > 0) {
      setCodeToInputs(digits, 0);
    }
  });

  // Fallback Autofill SMS : détection par intervalle pour iOS Safari
  let lastValue = '';
  setInterval(() => {
    const firstInput = inputs[0];
    if (firstInput && firstInput.value && firstInput.value !== lastValue) {
      const val = firstInput.value.replace(/[^0-9]/g, '');
      if (val.length > 1) {
        setCodeToInputs(val, 0);
      }
      lastValue = firstInput.value;
    }
  }, 300);
</script>