This post is also avalaible in : English

Les expériences les plus douloureuses sur le mobile sont très certainement celles sur les formulaires en ligne qu’il faut remplir ici et là. Et si vous optimisiez ces expériences étape par étape ? Aujourd’hui, je vous propose une solution pour la saisie d’un code unique temporaire : Vous recevez le SMS avec un code, votre clavier vous le suggère – un seul tap, c’est fait ! Ok, c’est parti !

Généralement, lorsque je propose un formulaire, court ou long, je veux optimiser l’expérience pour la rendre la moins douloureuse possible. Pour ce faire, j’étudie le comportement de mes utilisateurs en testant des solutions et en profitant des fonctionnalités que les terminaux (mobiles notamment) apportent avec les nouvelles versions, d’un point de vue matériel et logiciel.

Même si cet article ressemble à un article technique, il est en partie une réponse après de la recherche utilisateur et de certaines règles d’ergonomie :

  • Le champ correspond à la longueur des données attendues,
  • La forme du champ aide à la découverte et facilite l’intention,
  • Aide à la saisie en guidant l’utilisateur,
  • Anticipation des erreurs par la prévention des erreurs de formatage.

La démonstration

« Un clic, c’est fait ! », oui, vous avez raison de ne pas me faire confiance, mais voici comment vous pouvez le tester.

  • Allez sur la page de démo sur CodePen: One Time Code Demo.
  • Envoyez-vous un SMS avec le texte suivant :
    Your verification code is 133-742
    The SMS text
  • Retournez à la page de démonstration et touchez l’un des champs.
  • Votre clavier doit vous suggérer le dernier code reçu.

Note : Parfois, android ne se soucie pas de tout ça, cela dépend de la surcouche de votre fournisseur ou constructeur, ou du clavier que vous avez installé et de ses paramètres. Mais si vous êtes sur la dernière version d’iOS, vous devriez pouvoir gérer cela comme un pro.

Comment le code unique par SMS fonctionne ?

Pour que la magie opère, le système d’exploitation expose le dernier code SMS reçu pour qu’il soit utilisé par des applications comme votre clavier. Si le formulaire actuel demande ce code, votre clavier s’adapte et propose le code comme suggestion.

Keyboard Code Suggestion The autocompleted fields

D’accord, mais comment demander ce code ? C’est assez simple, mais vous ne pouvez pas le deviner si vous ne connaissez pas les possibilités de HTML5. Parmi les nombreuses connaissances peu communes, on trouve les attributs de autocomplete et ses nombreuses valeurs. Son utilisation n’est pas si difficile, vous devez correspondre à la valeur d’une liste existante de valeurs standard qui ne cesse de s’allonger.

<label for="otc">SMS Code Received</label>
<input type="text" autocomplete="one-time-code" id="otc" name="otc" aria-label="Enter the 6 Digits code you received by SMS">
A One Time Code example

Une fois que vous avez fait cela, votre champ SMS One Time Code est prêt. Pas besoin d’en faire plus ici.

Mais comme je vous l’ai dit, j’aime être complet dans ma proposition d’interface, et j’ai décidé d’aller plus loin avec une forme à 6 entrées.

Formulaire en forme de code SMS à 6 chiffres

J’ai décidé d’utiliser un formulaire à 6 champs, mais avec un formulaire à 1 entrée proposant un design comme si c’était 6 champs côté ) côté aurait aussi pu suffire. Comme je le dis souvent, c’est la magie du HTML/CSS et du JS, 1 solution, plusieurs façons d’y parvenir.

Pourquoi ferais-je cela ? Parce que lorsque l’utilisateur reçoit son code SMS, le format est assez clair : 3 chiffres, 1 tiret, 3 chiffres. Je veux que mon formulaire ressemble au code reçu.

<form class="otc" name="one-time-code" action="#">
	<fieldset>
		<legend>Validation Code</legend>
		<label for="otc-1">Number 1</label>
		<label for="otc-2">Number 2</label>
		<label for="otc-3">Number 3</label>
		<label for="otc-4">Number 4</label>
		<label for="otc-5">Number 5</label>
		<label for="otc-6">Number 6</label>

		<div>
			<!-- https://developer.apple.com/documentation/security/password_autofill/enabling_password_autofill_on_an_html_input_element -->
			<input type="number" pattern="[0-9]*"  value="" inputtype="numeric" autocomplete="one-time-code" id="otc-1" required>

			<!-- Autocomplete not to put on other input -->
			<input type="number" pattern="[0-9]*"  value="" inputtype="numeric" id="otc-2" required>
			<input type="number" pattern="[0-9]*"  value="" inputtype="numeric" id="otc-3" required>
			<input type="number" pattern="[0-9]*"  value="" inputtype="numeric" id="otc-4" required>
			<input type="number" pattern="[0-9]*"  value="" inputtype="numeric" id="otc-5" required>
			<input type="number" pattern="[0-9]*"  value="" inputtype="numeric" id="otc-6" required>
		</div>
	</fieldset>
</form>
The HTML Form

Comme vous pouvez le voir, j’ai utilisé un fieldset pour regrouper les champs sous une même legend. Chaque entrée a son propre label lié grâce aux attributs for et id, comme il est recommandé pour des raisons d’accessibilité. Malgré toutes ces attentions, je ne suis pas sûr que la solution soit si bonne pour les lecteurs d’écran (peut-être un peu trop lourde à lire), je vais essayer de faire d’autres tests après la publication et je reviendrai vers vous plus tard.

Pour l’instant, vous ne devriez pas avoir la forme la plus sexy de votre vie. Les champs numériques et toutes ces étiquettes… Mais passons maintenant à la partie CSS.

.otc label {
	border: 0;
	clip: rect(1px, 1px, 1px, 1px);
	-webkit-clip-path: inset(50%);
	clip-path: inset(50%);
	height: 1px;
	margin: -1px;
	overflow: hidden;
	padding: 0;
	position: absolute;
	width: 1px;
	white-space: nowrap;
}
This hides label visually

La première chose que j’ai faite est de cacher les label visuellement, mais pas pour les lecteurs d’écran. Code proposé par ffoodd et utilisé dans tous mes projets depuis un certain temps.

.otc {
	position: relative;
	width: 320px;
	margin: 0 auto;
	font-size: 2.65em;
}

.otc fieldset {
	border: 0;
	padding: 0;
	margin: 0;
}

.otc div {
	display: flex;
	align-items: center;
}

.otc legend {
	margin: 0 auto 1em;
	color: #5555FF;
}
Global Form width and fieldset styling

J’ai ensuite donné une dimension à mon formulaire pour le centrer, et j’ai supprimé certains styles par défaut des champs et des légendes (principalement les bordures et les espaces), la mise en page flexbox permet d’anticiper le petit alignement des tirets entre les champs.

input[type="number"] {
	width: .82em;
	line-height: 1;
	margin: .1em;
	padding: 8px 0 4px;
	font-size: 2.65em;
	text-align: center;
	appearance: textfield;
	-webkit-appearance: textfield;
	border: 2px solid #BBBBFF;
	color: purple;
	border-radius: 4px;
}

input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
Code to style input number

Ce code sert à styliser nos entrées de type number, avec une petite astuce pour notre ami Chrome qui doit être stylisé grâce à des pseudo-classes (l.15). De cette façon, vous devriez déjà avoir quelque chose de plus attrayant : nous avons dimensionné les champs (principalement grâce à la taille de la police) et supprimé les boutons d’incrément. Il ne nous reste plus qu’à « grouper » les champs 3 par 3. Faisons cela, d’accord ?

input[type="number"]:nth-of-type(n+4) {
	order: 2;
}

.otc div::before {
	content: '';
	height: 2px;
	width: 24px;
	margin: 0 .25em;
	order: 1;
	background: #BBBBFF;
}
The little dash in between

Ici j’ajoute un tiret grâce à un pseudo-élément ::before et je crée visuellement 2 groupes. Le pseudo-élément agit comme un autre enfant à côté de toutes les entrées. Si vous ne gérez pas l’ordre grâce à la propriété order, le tiret est à la première position. Pour le faire apparaître à la quatrième place, j’ai dû dire aux 3 derniers champs d’être à la deuxième place (order: 2), le tiret à la première (order: 1) et les autres sont par défaut à la place zéro (order: 0). Ouais, les développeurs vous savez… le 0 est le premier dans cette suite logique 😜

Et voilà !

Mettons du JavaScript pour améliorer l’expérience de l’utilisateur

En divisant un champs en 6 champs, nous avons voulu améliorer l’assistance en guidant l’utilisateur pour qu’il comprenne plus rapidement le format attendu. Une information plus facile à scanner. Mais pour l’instant, le formulaire n’est pas du tout utilisable, nous l’avons rendu pire que mieux si on en reste là.

Nous devons couvrir plusieurs choses ici :

  • Lorsque je remplis un champ, je passe au suivant,
  • Lorsque je supprime un contenu (backspace), je passe au champ précédent,
  • Lorsque je clique sur un champ vide, si le champ précédent est vide, je me place dessus,
  • Lorsque je copie/colle un code dans le premier champ, je dois couper le code et remplir les autres entrées.

Le code JavaScript proposé ci-dessous a un peu évolué si vous le comparez au CodePen. Celui de CodePen est plus avancé et également commenté. Je mettrai à jour l’article prochainement.

Allons-y ! Je vous montre le code et vous l’explique juste après.

let in1 = document.getElementById('otc-1'),
    ins = document.querySelectorAll('input[type="number"]');

ins.forEach(function(input) {
	/**
	 * Control on keyup to catch what the user intent to do.
	 * I could have check for numeric key only here, but I didn't.
	 */
	input.addEventListener('keyup', function(e){
		// Break if Shift, Tab, CMD, Option, Control.
		if (e.keyCode === 16 || e.keyCode == 9 || e.keyCode == 224 || e.keyCode == 18 || e.keyCode == 17) {
			 return;
		}
		
		// On Backspace or left arrow, go to the previous field.
		if ( (e.keyCode === 8 || e.keyCode === 37) && this.previousElementSibling && this.previousElementSibling.tagName === "INPUT" ) {
			this.previousElementSibling.select();
		} else if (e.keyCode !== 8 && this.nextElementSibling) {
			this.nextElementSibling.select();
		}
	});
	
	/**
	 * Better control on Focus
	 * - don't allow focus on other field if the first one is empty
	 * - don't allow focus on field if the previous one if empty (debatable)
	 * - get the focus on the first empty field
	 */
	input.addEventListener('focus', function(e) {
		// If the focus element is the first one, do nothing
		if ( this === in1 ) return;
		
		// If value of input 1 is empty, focus it.
		if ( in1.value == '' ) {
			in1.focus();
		}
		
		// If value of a previous input is empty, focus it.
		// To remove if you don't wanna force user respecting the fields order.
		if ( this.previousElementSibling.value == '' ) {
			this.previousElementSibling.focus();
		}
	});
});

/**
 * Handle copy/paste of a big number.
 * It catches the value pasted on the first field and spread it into the inputs.
 */
in1.addEventListener('input', function(e) {
	let data = e.data || this.value; // Chrome doesn't get the e.data, it's always empty, fallback to value then.
	if ( ! data ) return; // Shouldn't happen, just in case.
	if ( data.length === 1 ) return; // Here is a normal behavior, not a paste action.
	
	for (i = 0; i < data.length; i++ ) {
		ins[i].value = data[i];
	}
});
The JavaScript to make this form smart

De la ligne 11 à 13, je ne fais plus rien avec les touches Shift, Tab, CMD, Option, Control pour éviter les problèmes. De la ligne 16 à 20, je gère les fonctions Précédent/Suivant. De 29 à 43, je m’occupe du focus sur les champs. L’idée ici est que si je tape/clique sur un champ autre que le premier, je force le focus sur le premier champ. C’est surtout pour les utilisateurs de smartphones. Mais pour être plus intelligent, si certains champs sont déjà remplis, nous mettons le focus sur le premier champ vide.

Les dernières lignes de JavaScript sont là pour gérer un copier/coller, ou l’autosuggestion du clavier d’un smartphone. Dans ce cas, tout le contenu du code est placé dans ce premier champ. Nous prenons la valeur de ce champ et la divisons pour distribuer les valeurs dans les champs suivants.

Code Unique : à retenir

Ok, nous l’avons fait. Mais n’oubliez pas que c’est un pattern issu d’un de mes besoins, elle n’est peut-être pas parfaite, mais pour mon cas d’utilisation, elle est suffisante. Quelques petites choses à rappeler et à emporter avec vous.

  • Utilisez autocomplete="one-time-code" pour déclencher l’autosuggestion de votre smartphone.
  • Utilisez une combinaison de pattern="[0-9]*" et inputtype="numeric" pour déclencher le clavier numérique de votre smartphone.
  • Lorsque vous créez des formulaires complexes, essayez de dimensionner/styliser le champ en fonction des données attendues.
  • N’oubliez pas de tester vos formulaires en y naviguant avec le clavier uniquement, et avec un lecteur d’écran.

Merci pour votre lecture. Je suis ouvert aux suggestions les commentaires ou sur Twitter.
Merci à Laurent pour les tests sur iOS.