How to Create An OTP Input Box in React/Next.js
Tags: javascript, react, nextjs
Published: 2023-07-23 07:52:44

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zs1ok0fuw22y8hl2he85.png)

## 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

```css

/** 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;
 }
}
```

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.

```html
<input id="input1" type="text" maxlength="1" name="input1" value="">
...
<input id="input6" type="text" maxlength="1" name="input6" value="">
```

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](https://cdn-images-1.medium.com/max/800/0*fJZGW0y8nDUVIIpm.gif)

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

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.

```javascript
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();
                }
            }
        }
    }
}
```

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.

```javascript
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}
        />
    );
};
```

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

```javascript
//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
    };
}
```

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.

```javascript
//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>
        </>
    );
}
```

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`

```javascript
//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;
```

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

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

export default function ResetPage(){
    return (
        <>
             <OTPInputGroup />
        </>
    )
}
```

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.

{% embed https://youtu.be/R5SziarxAu4 %}

## 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](https://github.com/Octagon-simon/otp-app)

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](https://www.linkedin.com/in/simon-ugorji-57a6a41a3/)

[Twitter](https://twitter.com/ugorji_simon)

[Facebook](https://web.facebook.com/simon.ugorji.106/)

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


![Off_you_go](https://miro.medium.com/v2/resize:fit:640/0*hGmzP7rpDfNo3pn1.gif)


Photo credit: Lautaro Andreani On Unsplash