An OTP (short for One-Time Password) input field is a common feature in web applications that require users to authenticate their identity. It's particularly useful in online banking, e-commerce websites, and social media platforms. OTPs are unique codes generated for each transaction and have a limited lifespan, usually lasting only a few minutes before expiring. This ensures that the code cannot be reused or intercepted by hackers.
As online security becomes a growing concern, more and more web developers are integrating OTP input fields into their applications to safeguard their users' sensitive information. In this post, we'll walk through creating an OTP input field using JavaScript DOM.
HTML Markup
To create the HTML markup for the OTP input field, we'll start with a div
element and give it a class of otp-input
. Inside this div
, we'll create four input fields with a class of otp__input
. Here's the code:
<div class="otp">
<input type="text" class="otp__input" maxlength="1" />
<input type="text" class="otp__input" maxlength="1" />
<input type="text" class="otp__input" maxlength="1" />
<input type="text" class="otp__input" maxlength="1" />
</div>
It's important to note that each input field only allows for a single digit to be entered. Luckily, we can utilize the built-in maxlength
attribute to ensure this limit is enforced.
Basic styles
In this example, we'll add two CSS classes: otp
and otp__input
.
The otp
class is responsible for holding the OTP input fields. We've set its display property to flex
to align and center the child elements both horizontally and vertically. We've also added a 1rem gap between each input field for visual separation, making it easier for users to enter their OTP code.
On the other hand, the otp__input
class is used to refer to the input fields in the OTP container. This class uses the text-align
property to center the text within the input fields.
.otp {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.otp__input {
text-align: center;
}
To make the OTP input field more visually appealing, we can remove the default border and outline styles. Instead, we can add a slender border at the bottom and increase the font size. Check out the example below to see how it could look.
.otp__input {
border: none;
border-bottom: 2px solid rgb(203 213 225);
font-size: 2rem;
outline: none;
}
Entering only one digit at a time
We want to make sure that only digits are entered into our OTP input fields. To make this happen, we first select all of the input fields in the OTP container using querySelectorAll('.otp__input')
. Then, we convert the NodeList
returned by querySelectorAll()
into an array using Array.from()
so that we can use array methods like forEach()
.
Next, we need to handle the keydown
event for each input. This event is triggered every time a key is pressed within an input field. Inside the handler, we check if the key pressed is a digit using the /^[0-9]{1}$/
regular expression pattern. If it's not a digit, we prevent its default behavior by calling e.preventDefault()
.
It's worth noting that we allow the use of the Backspace and Delete keys to remove the current digit, just as you would expect. So, we also compare the pressed key with them.
Here's an example of what the keydown event handler looks like:
const otpEle = document.getElementById('otp');
const inputs = [...otpEle.querySelectorAll('.otp__input')];
const handleKeyDown = (e) => {
if (!/^[0-9]{1}$/.test(e.key) && e.key !== 'Backspace' && e.key !== 'Delete') {
e.preventDefault();
}
};
inputs.forEach((input) => {
input.addEventListener('keydown', handleKeyDown);
});
Selecting the digit automatically
When a user clicks on an input field within the OTP container, it becomes active. To make it easier for users to replace the current digit in the input field, we can use the focus
event to automatically select the input field's content when it is focused.
We do this by using the select()
method of the target element inside the event handler. This selects the current digit in the input field, so users can replace it without having to delete it first.
const handleFocus = (e) => {
e.target.select();
};
inputs.forEach((input) => {
input.addEventListener('focus', handleFocus);
});
Pasting all digits at the same time
It's common for users to copy a number and then paste it into a field. To handle this situation, we can use the paste
event to capture when a user pastes text into an input field. Then, we split the value of the first input field into an array of digits and set the value of each remaining input field to the corresponding digit in the array.
To give you an idea of how this works, here's an example of what the handlePaste()
function might look like:
const handlePaste = (e) => {
e.preventDefault();
const pastedText = e.clipboardData.getData('text');
if (!/^[0-9]{4}$/.test(pastedText)) {
return;
}
const digits = pastedText.split('');
inputs.forEach((input, index) => input.value = digits[index]);
inputs[inputs.length - 1].focus();
};
inputs.forEach((input) => {
input.addEventListener('paste', handlePaste);
});
In this function, we start by preventing the default behavior of pasting text with e.preventDefault()
. Then, we use e.clipboardData.getData('text')
to get the copied text, which we then split into an array of individual characters using split('')
.
Please remember that we only allow pasting of four-digit numbers. We do this by checking if the copied text matches the pattern of /^[0-9]{4}$/
.
Next, we'll loop through each input field and fill it with the corresponding digit from our array. Additionally, the handler will automatically focus on the last input field, making the process even smoother.
Navigating between fields
Currently, the OTP input provides some useful functionalities. However, we want to take it further by adding more shortcuts to make the user experience smoother.
Our first improvement will be to automatically focus on the next field when the user enters a digit or presses the right arrow key. This way, users can quickly enter the entire OTP without having to manually switch between fields. To do this, we'll handle the keyup
event on each input field in the OTP container. This event is triggered every time a key is released within an input field.
In the event handler, we'll check if the pressed key is a digit or the right arrow key. If it is, we'll jump to the next field. We'll only do this if we haven't reached the last input field yet, which we'll determine by comparing the current index with the total number of input fields.
Here's what the function might look like:
const handleKeyUp = (e) => {
const index = inputs.indexOf(e.target);
switch (e.key) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case 'ArrowRight':
if (index < inputs.length - 1) {
// Jump to the next field
inputs[index + 1].focus();
}
break;
default:
break;
}
};
inputs.forEach((input) => {
input.addEventListener('keyup', handleKeyUp);
});
Similarly, we want to bring the user back to the previous field when they press the left arrow or Backspace key. Just make sure to check if we aren't on the first field by comparing the index with 0.
switch (e.key) {
// ...
case 'ArrowLeft':
case 'Backspace':
if (index > 0) {
// Jump to the previous field
inputs[index - 1].focus();
}
break;
}
How about making it easier to navigate the first and last fields? We can add support for the Home and End keys to accomplish this. We just need to include a few extra checks in our switch case
statement.
switch (e.key) {
// ...
case 'Home':
// Jump to the first field
inputs[0].focus();
break;
case 'End':
// Jump to the first field
inputs[inputs.length - 1].focus();
break;
}
And that's it! We've successfully created an OTP input field using JavaScript DOM. It's time to see the final result of the steps we've been following so far.
See also
It's highly recommended that you visit the original post to play with the interactive demos.
If you found this series helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!
If you want more helpful content like this, feel free to follow me:
Top comments (0)