Create an OTP Input with Javascript

Shuqi Khor
4 min readMay 19, 2023

--

It’s easy, you just need 6 input boxes…

But wait! You actually need some more codes to make them behave like a normal input box.

Let’s start with literally 6 input boxes anyway:

<div id="otp-input">
<input type="number" step="1" min="0" max="9" autocomplete="no" pattern="\d*">
<input type="number" step="1" min="0" max="9" autocomplete="no" pattern="\d*">
<input type="number" step="1" min="0" max="9" autocomplete="no" pattern="\d*">
<input type="number" step="1" min="0" max="9" autocomplete="no" pattern="\d*">
<input type="number" step="1" min="0" max="9" autocomplete="no" pattern="\d*">
<input type="number" step="1" min="0" max="9" autocomplete="no" pattern="\d*">
</div>
<input type="hidden" name="otp">

…and a hidden input to collect the final value.

Note that we don’t set a maxlength attribute here because we want to allow users to paste a code copied from somewhere else.

Also on a sidenote, the pattern attribute here would trigger numpad keyboard on mobile devices.

Now, when the user types a digit, the focus is supposed to jump to the next input box. Let’s do it with “input” event:

const inputs = document.querySelectorAll("#otp-input input");
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i];

input.addEventListener("input", function () {
// handling normal input
if (input.value.length == 1 && i+1 < inputs.length) {
inputs[i+1].focus();
}
/* ... other codes (a) ... */
});
/* ... other codes (b) ... */
}

“Input” event will be fired whenever a value change in the input is detected. It is value-driven, not keyboard-driven.

Next, as mentioned above, when the user pastes a code copied from SMS or email, we should split them up and distribute to each of the next boxes.

Let’s put these codes in the [other codes (a)] block in the example above:

// if a value is pasted, put each character to each of the next input
if (input.value.length > 1) {
// sanitise input
if (isNaN(input.value)) {
input.value = "";
return;
}

// split characters to array
const chars = input.value.split('');

for (let pos = 0; pos < chars.length; pos++) {
// if length exceeded the number of inputs, stop
if (pos + i >= inputs.length) break;

// paste value
let targetInput = inputs[pos + i];
targetInput.value = chars[pos];
}

// focus the input next to the last pasted character
let focus_index = Math.min(inputs.length - 1, i + chars.length);
inputs[focus_index].focus();
}

On top of that, we also want to handle keyboard events for when left or right arrow is pressed. Let’s do it with “keydown” event:
(these should be written in [other codes (b)] block)

input.addEventListener("keydown", function (e) {
// left button
if (e.keyCode == 37) {
if (i > 0) {
e.preventDefault();
inputs[i-1].focus();
inputs[i-1].select();
}
return;
}

// right button
if (e.keyCode == 39) {
if (i+1 < inputs.length) {
e.preventDefault();
inputs[i+1].focus();
inputs[i+1].select();
}
return;
}

/* ... other codes (c) ... */
}

To make it behave like a single input, when [backspace] or [delete] key is pressed, we need to shift the values in the next boxes too:

// backspace button
if (e.keyCode == 8 && input.value == '' && i != 0) {
// shift next values towards the left
for (let pos = i; pos < inputs.length - 1; pos++) {
inputs[pos].value = inputs[pos + 1].value;
}

// clear previous box and focus on it
inputs[i-1].value = '';
inputs[i-1].focus();
return;
}

// delete button
if (e.keyCode == 46 && i != inputs.length - 1) {
// shift next values towards the left
for (let pos = i; pos < inputs.length - 1; pos++) {
inputs[pos].value = inputs[pos + 1].value;
}

// clear the last box
inputs[inputs.length - 1].value = '';

// select current input
input.select();

// disallow the event delete the new value
e.preventDefault();
return;
}

On a final touch, let’s define a function to update the hidden input:

function updateInput() {
let inputValue = Array.from(inputs).reduce(
function (otp, input) {
otp += (input.value.length) ? input.value : ' ';
return otp;
},
""
);
document.querySelector("input[name=otp]").value = inputValue;
}

…and place updateInput() call before every return that might see a change in final value.

That’s it!

Note that we could also use a for loop in place of <array>.reduce() but I like to make it a habit to use these array functions whenever applicable.

To wrap up, let’s wrap them up (pun intended) in a self-invoking function for better variable scoping, and it’s done!

If you want more, here’s an extended version of these codes that I have put up on GitHub:

…which would also handle text and password input types, plus the ability to set and get the value programatically.

Hope you like it! And hope you get a nice OTP every time!

--

--