Logo

dev-resources.site

for different kinds of informations.

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

Published at
7/27/2024
Categories
react
javascript
webdev
component
Author
shivishbrahma
Categories
4 categories in total
react
open
javascript
open
webdev
open
component
open
Author
13 person written this
shivishbrahma
open
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

component Article's
30 articles in total
Favicon
Build a note app with JavaScript component.
Favicon
Key characteristic of Component-Based Architecture
Favicon
React Component Libraries: Overview of 19 Top Libs
Favicon
Styling Components in React 🧢
Favicon
Building a Seamless OTP Input Field in React: A Step-by-Step Guide
Favicon
MithrilJS component with state management
Favicon
[Off Topic] Nano introdução do framework Angular para Devs do back
Favicon
Comparing Vue Component Documentation tools
Favicon
Laravel 11 Custom Component File Structure
Favicon
Building Message Component in Vue3
Favicon
Aplicando paginação em um componente Select
Favicon
How much does it cost to repair an outdoor LED display?
Favicon
Global toast in Vue3
Favicon
Unveiling the Hidden Gem of React: The Power of Compound Components
Favicon
Controlled vs Uncontrolled Components in React
Favicon
React components -(Class component v/s function component)
Favicon
3 Ways to Create React Components with Bit
Favicon
Client or Server component ?
Favicon
Desenvolvimento de Componentes Assíncronos com Vue.js
Favicon
NAND Flash vs NOR Flash: Differences between them
Favicon
Link Component in React Router
Favicon
Guia de Components - para quem tem pressa!
Favicon
How to exchange data between Angular components
Favicon
Component Testing with Cypress and Reactjs
Favicon
React - Higher Order Components (HOC)
Favicon
How to Create Component Library Like Material UI or Mantine UI?
Favicon
Looking forward to adding Algolia's DOCSEARCH to Mantine DataTable
Favicon
Cypress Component Testing vs React Testing Library - the complete comparison
Favicon
Creating Custom Component for NPS Feedback
Favicon
Maximize your Angular code reusability using <NgTemplateOutlet>

Featured ones: