DEV Community

Cover image for Build an OTP input field
Phuoc Nguyen
Phuoc Nguyen

Posted on • Originally published at phuoc.ng

Build an OTP input field

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)