DEV Community

Cover image for Building a Seamless OTP Input Field in React: A Step-by-Step Guide
Purbayan Chowdhury
Purbayan Chowdhury

Posted on • Edited on

Building a Seamless OTP Input Field in React: A Step-by-Step Guide

In today's digital age, ensuring secure access to online services is paramount, and One-Time Passwords (OTPs) play a crucial role in this process. OTPs provide an additional layer of security by requiring users to enter a unique code sent to their mobile device or email. Implementing an OTP input field that is both user-friendly and secure can significantly enhance the user experience of your application.

In this step-by-step guide, we will walk you through the process of building a seamless OTP input field in React. We will cover essential features such as automatically moving to the next input box upon entering a digit, handling backspace for deleting digits, navigating between input fields with arrow keys, and efficiently handling paste operations. By the end of this tutorial, you will have a robust OTP input component ready to integrate into your React application, ensuring a smooth and intuitive user experience for your users.

Features of OTPInput Component

  • Flexible Length: The OTP input field will accommodate either 4 or 6 individual text boxes, each representing a single digit.
  • Backspace Handling: Efficiently delete digits and automatically move the focus to the previous input box when the backspace key is pressed.
  • Automatic Navigation: Seamlessly move to the next input box as soon as a digit is entered, ensuring a smooth and intuitive user experience.
  • Arrow Key Navigation: Use the left and right arrow keys to easily navigate between input boxes, with the selected input box receiving focus.
  • Paste Support: Effortlessly paste an entire OTP code into the input field, with the component intelligently distributing the digits across the appropriate text boxes.

By incorporating these features, our OTP input component will enhance user interaction, making the process of entering and verifying OTPs both quick and user-friendly.

Getting Started

Starting with the basic cra-template of create-react app

OTPInput.jsx

import React from 'react'
import PropTypes from 'prop-types'

function OTPInput({...otherProps}) {
  return (
    <div className="OTPInput" {...otherProps}></div>
  );
}

OTPInput.propTypes = {};

export default OTPInput;
Enter fullscreen mode Exit fullscreen mode

A simple functional component with otherProps.

Implementation

Input Fields for Each Digit

We will add input fields for each digit in the OTP using length prop spreading over list of numbers from 1 to length . A shortcut to generate numbers from 1 to length is Array(length).fill(i + 1).

...
function OTPInput({length, ...otherProps}) {
    const [otp, setOtp] = React.useState("");
    const inputs = [];

    return (
        <div className="OTPInput" {...otherProps}>
            {Array(length)
                .fill((_, i) => i + 1)
                .map((_, index) => (
                    <input
                        className="OTPInput__input"
                        key={index}
                        type="text"
                        maxLength="1"
                        value={otp[index]?.toString() || ""}
                        placeholder="0"
                        ref={(input) => (inputs[index] = input)}
                    />
                ))}
        </div>    
    );
}

OTPInput.propTypes = {
    length: PropTypes.number
};

OTPInput.defaultProps = {
    length: 6
};
...
Enter fullscreen mode Exit fullscreen mode

Introducing a state for otp and a setOtp function to update the state. We will use otp to store the current OTP value and setOtp to update the state. Each input element value is indexed over this state variable. The inputs array will be used to store the input elements used to render the OTP. The length prop is used to determine the number of input fields to render and is set to 6 by default.

Styling OTP Inputs

OTPInput.css

.OTPInput__input {
    width: 40px;
    height: 40px;
    text-align: center;
    margin: 0 5px;
    font-size: 20px;
}
Enter fullscreen mode Exit fullscreen mode
...
import './OTPInput.css';
...
Enter fullscreen mode Exit fullscreen mode

The OTPInput__input class is used to style the input elements.

Handling Input

...
function OTPInput({length, pattern, ...otherProps}) {
    ...

    const handleChange = (element, index) => {
        const value = element.value;
        if (!pattern.test(value)) return; // Only allow digits

        let newOtp = [...otp];
        newOtp[index] = value;

        setOtp(newOtp);

        // Move to the next input field
        if (value && index < length - 1) {
            inputs[index + 1].focus();
        }
    };

    return (
        <div className="OTPInput" {...otherProps}>
            {Array(length)
                .fill((_, i) => i + 1)
                .map((_, index) => (
                    <input
                        className="OTPInput__input"
                        key={index}
                        type="text"
                        maxLength="1"
                        value={otp[index]?.toString() || ""}
                        onChange={(e) => handleChange(e.target, index)}
                        placeholder="0"
                        ref={(input) => (inputs[index] = input)}
                    />
                ))}
        </div>
    );
}

OTPInput.propTypes = {
    ...
    pattern: PropTypes.instanceOf(RegExp)
};

OTPInput.defaultProps = {
    ...
    pattern: /\d/
};
...
Enter fullscreen mode Exit fullscreen mode

The pattern prop is used to validate the input value, which is set to /\d/ by default. The function handleChange updates the otp state with the new value, avoids the change if it doesn't match the pattern, also sets the focus to the next input field.

Handling Paste

...
function OTPInput({length, pattern, ...otherProps}) {
    ...
    const handlePaste = (e) => {
        e.preventDefault();
        const paste = e.clipboardData.getData("text");
        if (!pattern.test(paste)) return; // Only allow digits

        const newOtp = paste.slice(0, length).split("");
        for (let i = 0; i < length; i++) {
            inputs[i].value = newOtp[i] || "";

            if (newOtp[i] && i < length - 1) {
                inputs[i + 1].focus();
            }
        }
        setOtp(newOtp);
    };

    return (
        <div className="OTPInput" {...otherProps} onPaste={handlePaste}>
            {Array(length)
                .fill((_, i) => i + 1)
                .map((_, index) => (
                    <input
                        className="OTPInput__input"
                        key={index}
                        type="text"
                        maxLength="1"
                        value={otp[index]?.toString() || ""}
                        onChange={(e) => handleChange(e.target, index)}
                        placeholder="0"
                        ref={(input) => (inputs[index] = input)}
                    />
                ))}
        </div>
    );
}
...
Enter fullscreen mode Exit fullscreen mode

The function handlePaste matches the pasted value with the pattern, updates the otp state from the pasted value and sets the focus to the next empty input field. The paste function is set to the whole OTPInput component.

Deleting Values and Moving Focus

...
function OTPInput({length, pattern, ...otherProps}) {
    ...

    const handleKeyDown = (e, index) => {
        if (e.key === "Backspace") {
            e.preventDefault();
            let newOtp = [...otp];
            newOtp[index] = "";
            setOtp(newOtp);

            if (index > 0) {
                inputs[index - 1].focus();
            }
        } else if (e.key === "ArrowLeft" && index > 0) {
            inputs[index - 1].focus();
        } else if (e.key === "ArrowRight" && index < length - 1) {
            inputs[index + 1].focus();
        }
    };
    ...

    return (
        <div className="OTPInput" {...otherProps} onPaste={handlePaste}>
            {Array(length)
                .fill((_, i) => i + 1)
                .map((_, index) => (
                    <input
                        className="OTPInput__input"
                        key={index}
                        type="text"
                        maxLength="1"
                        value={otp[index]?.toString() || ""}
                        onChange={(e) => handleChange(e.target, index)}
                        onKeyDown={(e) => handleKeyDown(e, index)}
                        placeholder="0"
                        ref={(input) => (inputs[index] = input)}
                    />
                ))}
        </div>
    );
}
...
Enter fullscreen mode Exit fullscreen mode

The function handleKeyDown is used for keypress events. For deletion via backspace, the function sets the focus to the previous input field and removes the value of the current input field. For moving focus via arrow keys, the function sets the focus to the next / previous input field.

Final Code

OTPInput.jsx

https://github.com/shivishbrahma/nuclear-reactor/blob/main/src/OTPInput/OTPInput.jsx

Use within App component

import OTPInput from './OTPInput';
import './App.css';

function App() {
    return (
        <div className="App">
            <OTPInput />
        </div>
    )
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Preview

OTPInput Preview

OTPInput Preview

Traversing through OTPInput Preview

Traversing through OTPInput Preview

Copy Paste in OTPInput Preview

Copy Paste in OTPInput Preview

References

Top comments (0)