Cet article est également disponible en : Français

Ones of the more painful experiences on mobile come with infinite online forms you have to fill in here and there. What about optimizing those experiences step by step? Today I propose to you a solution for One Time Code input: You receive the SMS with a code, your keyboard suggest-it to you, one tap, done! Ok let’s go!

Usually when I propose a form, short or long, I want to optimize the experience to make it as less painful as possible. To do so I study the behavior of my users by testing solutions and by enjoying the features that devices bring with new hardware and software versions.

Even if this article looks like a technical one, it comes from user research and some usability rules:

  • Input form matches the lenght of the expected data,
  • The shapes help the discoverability and ease the intent,
  • Input assistance by guiding the user,
  • Error anticipation by preventing formatting errors.

The demonstration

“One tap, done!”, yes, you are in right not to trust me, but here is how you can test it.

  • Go to the demonstration page on CodePen: One Time Code Demo.
  • Send yourself a SMS with the following text:
    Your verification code is 133-742
    The SMS text
  • Go back to demonstration page and touch one of the fields.
  • Your keyboard should suggest you the last code received.

Sometimes, android just doesn’t care about all of that, it depends on the over-layer of your provider or constructor, or the keyboard you installed and its settings. But if you are on the latest version of iOS, you should be able to manage that like a pro.

How One Time SMS code works?

To make the magic happen, the operating system exposes the last received SMS code to be used by applications like your keyboard. If the current form input asks for this code, your keyboard adapts and proposes the code as keyboard-suggestion.

Keyboard Code Suggestion The autocompleted fields

Ok, but how do you ask for this code? It’s quite simple, but you can’t guess it if you don’t know HTML5 possibilities. Among a lot of uncommon knowledges lays the autocomplete attributes and its numerous values. It’s usage is not that hard, you have to match the value of an existing growing list of standard values.

<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

Once you did that, your One Time Code SMS field is ready. No need for more here.

But as I told you, I like being complete in my interface proposition, and I decided to go further with a 6 inputs shaped form.

6 digit SMS code shaped form

I decided to go with a 6 inputs form, but with a 1 input form styled like if it was 6 fields would’ve worked too I suppose. Like I usually say, that’s the magic of HTML/CSS and JS, 1 solution, several ways to reach it.

Why would I do that? Because when the user receive their SMS code, the format is pretty clear: 3 digits, dash, 3 digits. I want my form looks like the code received.

<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

As you can see, I used a fieldset to group the fields under the same legend. Each input has its own label linked thanks to the for and id attributes, as it is recommend for accessibility reason. Despite all those attentions, I’m not sure the solution is that good for screen-readers (maybe a bit too heavy to read), I’ll try to test further after publication and come back to you later.

At the moment, you shouldn’t have the sexiest form of your life. Numeric fields and all those labels… But let’s jump into the CSS part now.

.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

First thing I did is to hide labels visually but not for screen-readers. Code proposed by ffoodd and used in all my projects for a while.

.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

Then I gave dimension to my form to center it, and removed some fieldset and legend default styles (mostly borders and spacings), the flex layout is to anticipate the little dash alignment between the fields.

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

This code is for styling our inputs of type number, with a little trick for our friend Chrome which needs to be styled thanks to pseudo-classes (l.15). That way, you should already have something more appealing: we sized the fields (mostly thanks to font-size) and removed the spin buttons. Now we just need to “group” the fields 3 by 3. Let’s do that, shall we?

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

Here I add a dash thanks to a pseudo-element ::before and visually create 2 groups. The pseudo-element act like another child next to all the inputs. If you don’t handle the order thanks to order property, the dash is at the first position. To make it appear at the fourth place I had to tell the 3 last input to be at the second place (order: 2), dash at the first one (order: 1) and others are by default at the zero place (order: 0). Yeah, developers, 0 is kind of first 😀

And voilà!

Put some JavaScript to improve user experience

By splitting the input into 6 inputs, we wanted to improve assistance by guiding the user helping them understand more quickly the format expecting. A more easy to scan information. But for now the form is not usable at all, we made it worst.

We need to cover several things here:

  • When I fill in a field, I go to the next one,
  • When I delete a content (backspace), I go to the previous field,
  • When I click on an empty field, if previous field is empty, I focus it,
  • When I copy/paste a code in the first field, I have to split and fill in the other inputs.

The JS code evolved a bit, it’s more advanced on CodePen, and it’s commented. Go have a look, I’ll update this article later.

Let’s go! I show you the code and explain it just after that.

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

From line 11 to 13 I do nothing more with the Shift, Tab, CMD, Option, Control keys to avoid issues. From line 16 to 20 I handle the prev/next functions. From 29 to 43 I handle the focus aspect. The idea here is that if I touch/click on a field other than the first one, I force the focus on the first field. This is mostly for smartphone users. But to be smarter, if some fields are already filled in, we focus the first empty one.

The last lines of JavaScript are here to handle a copy/paste action, or the keyboard autosuggestion of a smartphone. In that case, all the code content is put into that first field. We take the value of that field and split it to distribute the values in the next fields.

One Time Code: Takeaways

Ok, we did it. But don’t forget this is a Proof of Concept of mine, it might not be perfect but for my case of use it’s way enough. Some little things to recall and take away with you as a TLD’NR.

  • Use autocomplete="one-time-code" to trigger smartphone OTC auto-suggestion.
  • Use a combination of pattern="[0-9]*" and inputtype="numeric" to trigger numeric keyboard on smartphone.
  • When you create rich forms, try to shape the input to the expected data.
  • Don’t forget to test your forms by navigating it with keyboard only, and with a screenreader.

Thanks for reading! I’m open to suggestions in the comments or on Twitter.
Thanks to Laurent for the iOS testing.

Going further with Web Forms and CSS

Did you know that you can style web form inputs like checkboxes or radio buttons with only CSS? Or that you can style input:file for instance? Go further with these articles: