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