DEV Community

Cover image for How to Create An OTP Input Box in React/Next.js
Simon Ugorji
Simon Ugorji

Posted on

How to Create An OTP Input Box in React/Next.js

Today, I will show you how you can add an OTP input box to your React JS / Next JS project.

Please make sure that you have a basic knowledge of React's hooks so you can keep up with this tutorial.

I will be using the basic HTML, CSS, and Javascript logic from this snippet on Codepen, converting them to components, and then using states to modify and update the values.

PREREQUISITES

This tutorial assumes that you have a new ReactJs / NextJs installed or that you have an existing React / NextJS project.
We will aim at building something like this and of course, you are entitled to your own styling ✨

Image description

SETTING UP

Before we start off, we need to import the CSS declarations that we would use to style our input boxes.

IMPORT STYLES

Within your project, please create a CSS file otpInputs.css and import the declarations below


/** otpInputs.css **/

.digitGroup {
display: flex;
gap: 10px;
justify-content: center;
margin-bottom: 20px;
}

.digitGroup input {
 outline: 0 !important;
 user-select: none !important;
 width: 50px;
 height: 50px;
 background-color: #C5DBF5;
 font-weight: bold !important;
 border-radius: 5px;
 border: none;
 line-height: 50px;
 text-align: center;
 font-size: 24px;
 color: #001E9A;
 margin: 0;
}

.digitGroup input:focus {
 border: 2px solid #001E9A !important;
}

.digitGroup input:active {
 border: 2px solid #001E9A !important;
}

.digitGroup .splitter {
 padding: 5px 0;
 color: rgb(0, 0, 0);
 font-size: 25px;
 margin: 0;
}

.prompt {
 margin-bottom: 20px;
 font-size: 20px;
 color: white;
}

.formSection {
 max-width: 500px;
 margin: auto;
 border-radius: 10px;
 box-shadow: 0px 0px 8px #ddd;
 padding: 20px;
}

.w-100 {
 width: 100%;
}

/* Media query for mobile devices */
@media (max-width: 480px) {
 .digitGroup {
  gap: 5px !important;
 }
 .digitGroup input {
  width: 40px !important;
 }
 .digitGroup .splitter {
  font-size: 25px !important;
 }
}
Enter fullscreen mode Exit fullscreen mode

Next, we need to create a component that will render the input boxes and apply the styling and attributes associated with each of them.

Before we do this, I want to explain the logic / how it works.

A sample input box rendered by the otpInput component will look like this.

<input id="input1" type="text" maxlength="1" name="input1" value="">
...
<input id="input6" type="text" maxlength="1" name="input6" value="">
Enter fullscreen mode Exit fullscreen mode

The id attribute helps us identify each input box and the maxLength attribute ensures that the input box allows just one value.

Create The Components

Now, I will go ahead and create a file otpInputs.js and within this file, I will create a component OTPInput that will render the input box.

I will pass in the props below;

  • id - This is the unique identifier for each input box.
  • previousId - This is the ID of the previous input box.
  • nextId - This is the ID of the next input box. value - This is the value of a particular input.
  • onValueChange - This is a callback passed as a prop that will run when the input value changes.
  • handleSubmit - This is a callback that will run when the form is being submitted.

So our file will have the component below, with the properties I have explained above - what a nice wordplay if you noticed 🙂

cheers

const OTPInput = ({ id, previousId, nextId, value, 
    onValueChange, handleSubmit }) => {
//... Rest of the magical logic goes here
}
Enter fullscreen mode Exit fullscreen mode

Now we need to add a very important logic to the component above.

We will use the keyup event which is fired when a key is released, and within this event, we will use the code emitted by this event (keyCode) to indicate the key that was pressed. With this code, we can either move to the next input box or go back to the previous input box using the previousId and nextId respectively.

If there's no other input box to serve as the next one in the queue, it will check if the attribute data-autosubmit is specified on the parent element that will group these boxes, and if the attribute is specified and it is set to true, it will submit the form.

Below I have attached the code with their respective explanations.

const OTPInput = ({ id, previousId, nextId, value, onValueChange, handleSubmit }) => {
    //This callback function only runs when a key is released
    const handleKeyUp = (e) => {
        //check if key is backspace or arrowleft
        if (e.keyCode === 8 || e.keyCode === 37) {
            //find the previous element
            const prev = document.getElementById(previousId);
            if (prev) {
                //select the previous element
                prev.select();
            }
        } else if (
            (e.keyCode >= 48 && e.keyCode <= 57) || //check if key is numeric keys 0 to 9
            (e.keyCode >= 65 && e.keyCode <= 90) || //check if key is alphabetical keys A to Z
            (e.keyCode >= 96 && e.keyCode <= 105) || //check if key is numeric keypad keys 0 to 9
            e.keyCode === 39 //check if key is right arrow key
        ) {
            //find the next element
            const next = document.getElementById(nextId);
            if (next) {
                //select the next element
                next.select();
            } else {
                //check if inputGroup has autoSubmit enabled
                const inputGroup = document.getElementById('OTPInputGroup');
                if (inputGroup && inputGroup.dataset['autosubmit']) {
                    //submit the form
                    handleSubmit();
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now it is time to return a value from our component. We will return a single input box as a JSX expression and depending on how many times this component is called, it will return the corresponding number of input boxes.

Here's the complete code for the OTPInput component.

const OTPInput = ({ id, previousId, nextId, value, onValueChange, handleSubmit }) => {
    //This callback function only runs when a key is released
    const handleKeyUp = (e) => {
        //check if key is backspace or arrowleft
        if (e.keyCode === 8 || e.keyCode === 37) {
            //find the previous element
            const prev = document.getElementById(previousId);
            if (prev) {
                //select the previous element
                prev.select();
            }
        } else if (
            (e.keyCode >= 48 && e.keyCode <= 57) || //check if key is numeric keys 0 to 9
            (e.keyCode >= 65 && e.keyCode <= 90) || //check if key is alphabetical keys A to Z
            (e.keyCode >= 96 && e.keyCode <= 105) || //check if key is numeric keypad keys 0 to 9
            e.keyCode === 39 //check if key is right arrow key
        ) {
            //find the next element
            const next = document.getElementById(nextId);
            if (next) {
                //select the next element
                next.select();
            } else {
                //check if inputGroup has autoSubmit enabled
                const inputGroup = document.getElementById('OTPInputGroup');
                if (inputGroup && inputGroup.dataset['autosubmit']) {
                    //submit the form
                    handleSubmit();
                }
            }
        }
    }
    return (
        <input
            id={id}
            name={id}
            type="text"
            className={Styles.DigitInput}
            value={value}
            maxLength="1"
            onChange={(e) => onValueChange(id, e.target.value)}
            onKeyUp={handleKeyUp}
        />
    );
};
Enter fullscreen mode Exit fullscreen mode

If you noticed, we invoked a callback function on the onChange attribute to set a new value with the corresponding input ID, if the value of the input box changed. This callback function is passed as a property in our component and with the help of state management on the parent component, we will be able to track and store all the values of the input boxes with their respective IDs.

Next, we need to create this parent component called OTPInputGroup which will group these input boxes and render them. By rendering, I mean that it will invoke the OTPInput 6 times because we need just 6 input boxes.

This parent component will also be responsible to store the functions below;

  • handleSubmit - This function handles the logic when the form is submitted
  • handleInputChange - This function handles the logic when a value of an input box changes
//Our parent component
const OTPInputGroup = () => {
    //state to store all input boxes    
    const [inputValues, setInputValues] = useState({
        input1: '',
        input2: '',
        input3: '',
        input4: '',
        input5: '',
        input6: '',
        // Add more input values here
    });
    //this function updates the value of the state inputValues
    const handleInputChange = (inputId, value) => {
        setInputValues((prevInputValues) => ({
            ...prevInputValues,
            [inputId]: value,
        }));
    };
    //this function processes form submission
    const handleSubmit = () => {
        // ... Your submit logic here
    };
}
Enter fullscreen mode Exit fullscreen mode

If you look at the code above, you will notice that we have a state inputValues that stores all the input boxes and their respective values using their IDs from input1 - input as an object.

Now, to be able to update this state, we have another function handleInputChange which requests the input ID and the value of the input and then updates the state accordingly. This function is passed directly to the child component OTPInput as a callback through the prop onValueChange so it can get executed from within the child component.

Next, we have the handleSubmit function which processes the logic when our form is submitted. You can add your API call within that function and perform other validations as well.

Now, let us render the child component 6 times and pass in the properties as well.

Here's the complete code for the parent component.

//Our parent component
const OTPInputGroup = () => {
    //state to store all input boxes    
    const [inputValues, setInputValues] = useState({
        input1: '',
        input2: '',
        input3: '',
        input4: '',
        input5: '',
        input6: '',
        // Add more input values here
    });
    //this function updates the value of the state inputValues
    const handleInputChange = (inputId, value) => {
        setInputValues((prevInputValues) => ({
            ...prevInputValues,
            [inputId]: value,
        }));
    };
    //this function processes form submission
    const handleSubmit = () => {
        // ... Your submit logic here
    };
    //return child component
        return (
        <>
            <div id='OTPInputGroup' className={Styles.digitGroup} data-autosubmit="true">
                <OTPInput
                    id="input1"
                    value={inputValues.input1}
                    onValueChange={handleInputChange}
                    previousId={null}
                    handleSubmit={handleSubmit}
                    nextId="input2"
                />
                <OTPInput
                    id="input2"
                    value={inputValues.input2}
                    onValueChange={handleInputChange}
                    previousId="input1"
                    handleSubmit={handleSubmit}
                    nextId="input3"
                />
                <OTPInput
                    id="input3"
                    value={inputValues.input3}
                    onValueChange={handleInputChange}
                    previousId="input2"
                    handleSubmit={handleSubmit}
                    nextId="input4"
                />
                {/* Seperator */}
                <span className={Styles.splitter}>&ndash;</span>
                <OTPInput
                    id="input4"
                    value={inputValues.input4}
                    onValueChange={handleInputChange}
                    previousId="input3"
                    handleSubmit={handleSubmit}
                    nextId="input5"
                />
                <OTPInput
                    id="input5"
                    value={inputValues.input5}
                    onValueChange={handleInputChange}
                    previousId="input4"
                    handleSubmit={handleSubmit}
                    nextId="input6"
                />
                <OTPInput
                    id="input6"
                    value={inputValues.input6}
                    onValueChange={handleInputChange}
                    previousId="input5"
                    handleSubmit={handleSubmit}
                />
            </div>
        </>
    );
}
Enter fullscreen mode Exit fullscreen mode

Now if you noticed, before we invoked the child component, we had to group the input boxes and then there's this attribute data-autosubmit which enables the form to submit when a user has entered the value for all input boxes.

When invoking the child component, we passed in the properties below;

  • id - This is the unique Identifier for each input box
  • value - This is the value of the input box. It gets its value through the inputValues state which is being updated using the handleValueChange function
  • onValueChange - This prop will serve as a callback function that will reference the parent function handleInputChange responsible for updating the inputValues state.
  • previousId - This is the ID of the previous input box. This is not applicable to the first input box which is input1.
  • handleSubmit - This prop will serve as a callback function that will reference the parent function handleSubmit responsible for submitting the form when all inputs contain values and autosubmit is set to true
  • nextId - This is the ID of the next input box. This is not applicable to the last input box which is input 6.

COMPLETE CODE

Here is the complete code for the file otpInputs.js

//otpInputs.js
import React, {useState} from "react";
import Styles from './otpInput.css'; //remove this line if you are using react

//Our parent component
const OTPInputGroup = () => {
    //state to store all input boxes    
    const [inputValues, setInputValues] = useState({
        input1: '',
        input2: '',
        input3: '',
        input4: '',
        input5: '',
        input6: '',
        // Add more input values here
    });
    //this function updates the value of the state inputValues
    const handleInputChange = (inputId, value) => {
        setInputValues((prevInputValues) => ({
            ...prevInputValues,
            [inputId]: value,
        }));
    };
    //this function processes form submission
    const handleSubmit = () => {
        // ... Your submit logic here
    };
    //return child component
        return (
        <>
            <div id='OTPInputGroup' className={Styles.digitGroup} data-autosubmit="true">
                <OTPInput
                    id="input1"
                    value={inputValues.input1}
                    onValueChange={handleInputChange}
                    previousId={null}
                    handleSubmit={handleSubmit}
                    nextId="input2"
                />
                <OTPInput
                    id="input2"
                    value={inputValues.input2}
                    onValueChange={handleInputChange}
                    previousId="input1"
                    handleSubmit={handleSubmit}
                    nextId="input3"
                />
                <OTPInput
                    id="input3"
                    value={inputValues.input3}
                    onValueChange={handleInputChange}
                    previousId="input2"
                    handleSubmit={handleSubmit}
                    nextId="input4"
                />
                {/* Seperator */}
                <span className={Styles.splitter}>&ndash;</span>
                <OTPInput
                    id="input4"
                    value={inputValues.input4}
                    onValueChange={handleInputChange}
                    previousId="input3"
                    handleSubmit={handleSubmit}
                    nextId="input5"
                />
                <OTPInput
                    id="input5"
                    value={inputValues.input5}
                    onValueChange={handleInputChange}
                    previousId="input4"
                    handleSubmit={handleSubmit}
                    nextId="input6"
                />
                <OTPInput
                    id="input6"
                    value={inputValues.input6}
                    onValueChange={handleInputChange}
                    previousId="input5"
                    handleSubmit={handleSubmit}
                />
            </div>
        </>
    );
}

//Our child component
const OTPInput = ({ id, previousId, nextId, value, onValueChange, handleSubmit }) => {
    //This callback function only runs when a key is released
    const handleKeyUp = (e) => {
        //check if key is backspace or arrowleft
        if (e.keyCode === 8 || e.keyCode === 37) {
            //find the previous element
            const prev = document.getElementById(previousId);
            if (prev) {
                //select the previous element
                prev.select();
            }
        } else if (
            (e.keyCode >= 48 && e.keyCode <= 57) || //check if key is numeric keys 0 to 9
            (e.keyCode >= 65 && e.keyCode <= 90) || //check if key is alphabetical keys A to Z
            (e.keyCode >= 96 && e.keyCode <= 105) || //check if key is numeric keypad keys 0 to 9
            e.keyCode === 39 //check if key is right arrow key
        ) {
            //find the next element
            const next = document.getElementById(nextId);
            if (next) {
                //select the next element
                next.select();
            } else {
                //check if inputGroup has autoSubmit enabled
                const inputGroup = document.getElementById('OTPInputGroup');
                if (inputGroup && inputGroup.dataset['autosubmit']) {
                    //submit the form
                    handleSubmit();
                }
            }
        }
    }
    return (
        <input
            id={id}
            name={id}
            type="text"
            className={Styles.DigitInput}
            value={value}
            maxLength="1"
            onChange={(e) => onValueChange(id, e.target.value)}
            onKeyUp={handleKeyUp}
        />
    );
};

export default OTPInputGroup;
Enter fullscreen mode Exit fullscreen mode

In our project, we can now import this component and use it.

import React from 'react';
import OTPInputGroup from './path/to/otpInput.js';

export default function ResetPage(){
    return (
        <>
             <OTPInputGroup />
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

That's all 🙂

If you followed the tutorial correctly, you should arrive at something like this. Go ahead and insert a value, you will notice that it moves you over to the next input box, and when you delete a value or use backspace, it moves you back to the previous input box, this is where the keyUp event comes to play.

BEFORE YOU GO…

If you are using NextJS, you can use the code as it is including the Styles already imported. But if you are using ReactJs, please modify the code by importing the styles directly and then modifying the CSS declarations.

Check out the code on GitHub

It's been a while since I wrote my last article. This is because I have some other projects currently that I am working on and it doesn't stop me from sharing the best practices used in each of them.

If you loved this article, please don't hesitate to follow me on my social handles. Let's build GREAT things together!

Happy coding!

LinkedIn

Twitter

Facebook

…Off you go now, do well to stop by some other time 🥲

Off_you_go

Photo credit: Lautaro Andreani On Unsplash

Top comments (1)

Collapse
 
isour profile image
isour

This code skips values, if you're typing fast