Hello Guys, Today in this post we'll build a simple and interactive PIN entry component in NEXT JS 13 (TypeScript). This component is commonly used for scenarios like entering a one-time password (OTP).
Before we start, make sure you have a basic understanding of NEXT JS and TypeScript. If you're new to these technologies, consider checking the official documentation and introductory tutorials.
Start by creating a new NEXT project using the Create Next App or your preferred setup. You can enter the app name and select the typescript
npx create-next-app@latest
Now, let's create the component that allows users to enter a PIN. We'll use state management to store each digit and refs for controlling the inputs navigation such as focus and blur.
// otp-input.tsx
import { useRef, useState } from 'react';
const maxPinLength = 4; // change as per your requirements
const OtpInput= () => {
const [password, setPassword] = useState<number[]>(Array(maxPinLength).fill(-1));
const inpRefs = useRef(null);
const [activeInput, setActiveInput] = useState(-1);
...
};
As mentioned, we initialize the state for the PIN, use refs for managing input focus/blur, and track the active input. We also have the constant “maxPinLength” to define the pin length.
Let's continue building the UI for the PIN input. We'll create a form with input fields for each digit of the PIN.
// otp-input.tsx
...
const OtpInput = () => {
...
return (
<div className='text-center flex flex-col justify-center items-center'>
<form onSubmit={handleSumit} ref={inpRefs} >
<div className='flex space-x-4'>
{password.map((digit, i) => (
<div key={i} className='w-14 h-14 relative rounded-lg overflow-hidden'>
<label htmlFor={`pin_${i}`} className={`absolute flex justify-center items-center text-2xl top-0 left-0 w-full h-full ${activeInput == i ? "bg-gray-400" : "bg-gray-200"} opacity-100`}>{digit !== -1 ? "🤫" : "😶"}</label>
<input className='w-full h-full text-center border-none outline-none' id={`pin_${i}`} type='password'
value={digit !== -1 ? digit : ""}
></input>
</div>
))}
</div>
<button className='mt-10 bg-gray-900 active:border-b-2 active:border-blue-100 outline-none text-white px-4 py-2 text-lg rounded uppercase'>
Continue
</button>
</form>
</div>
);
};-
In this section, we create a form with input fields for each digit of the PIN by looping over the password array. We also handle the visual representation of filled and empty PIN digits.
Let's implement the logic for handling PIN input interactions, such as focus, blur, change, and key events.
// otp-input.tsx
...
const OtpInput= () => {
...
return (
<div className='text-center flex flex-col justify-center items-center'>
<h2 className='text-6xl font-bold mb-10'>Enter PIN</h2>
<form onSubmit={handleSumit} ref={inpRefs} >
<div className='flex space-x-4'>
{password.map((digit, i) => (
<div key={i} className='w-14 h-14 relative rounded-lg overflow-hidden'>
<label htmlFor={`pin_${i}`} className={`absolute flex justify-center items-center text-2xl top-0 left-0 w-full h-full ${activeInput == i ? "bg-gray-400" : "bg-gray-200"} opacity-100`}>{digit !== -1 ? "🤫" : "😶"}</label>
<input
onFocus={() => setActiveInput(i)}
onBlur={() => setActiveInput(-1)}
onKeyDown={(e) => handleKeyDown(e, i)}
onChange={(e) => handleChange(e, i)}
className='w-full h-full text-center border-none outline-none' id={`pin_${i}`} type='password'
value={digit !== -1 ? digit : ""}
></input>
</div>
))}
</div>
<button className='mt-10 bg-gray-900 active:border-b-2 active:border-blue-100 outline-none text-white px-4 py-2 text-lg rounded uppercase'>
Continue
</button>
</form>
</div>
);
};
In this section, we handle focus, blur, and key events for each PIN input. On focus we set the active input to index and onBlur we set it to -1
Now, let's implement the event handlers for different interactions.
// otp-input.tsx
...
const OtpInput= () => {
...
const handleKeyDown = (e:any, i: number)=>{
if (e.key == "Backspace") {
let pass = password
pass[i] = -1
setPassword(pass)
setActiveInput(i - 1)
if (i != 0) {
let nextInput = inpRefs?.current?.[i - 1]
//@ts-ignore
nextInput?.focus()
} else {
//@ts-ignore
inpRefs?.current?.[i].blur()
}
}
}
const handleChange=(e:any)=>{
// @ts-ignore
let v = e.nativeEvent["data"]
let pass = password
let value = parseInt(v)
if (!isNaN(value)) {
pass[i] = value
setPassword(pass)
setActiveInput(i + 1)
// Once the input finishes it focuses button which is the next element in the form
let nextInput = inpRefs?.current?.[i + 1]
//@ts-ignore
nextInput?.focus()
}
}
return (
<div className='text-center flex flex-col justify-center items-center'>
<h2 className='text-6xl font-bold mb-10'>Enter PIN</h2>
<form onSubmit={handleSumit} ref={inpRefs} >
<div className='flex space-x-4'>
{password.map((digit, i) => (
<div key={i} className='w-14 h-14 relative rounded-lg overflow-hidden'>
<label htmlFor={`pin_${i}`} className={`absolute flex justify-center items-center text-2xl top-0 left-0 w-full h-full ${activeInput == i ? "bg-gray-400" : "bg-gray-200"} opacity-100`}>{digit !== -1 ? "🤫" : "😶"}</label>
<input
onFocus={() => setActiveInput(i)}
onBlur={() => setActiveInput(-1)}
onKeyDown={(e) => handleKeyDown(e, i)}
onChange={(e) => handleChange(e, i)}
className='w-full h-full text-center border-none outline-none' id={`pin_${i}`} type='password'
value={digit !== -1 ? digit : ""}
></input>
</div>
))}
</div>
<button className='mt-10 bg-gray-900 active:border-b-2 active:border-blue-100 outline-none text-white px-4 py-2 text-lg rounded uppercase'>
Continue
</button>
</form>
</div>
);
};
In this section, we implement event handlers one is onChange where we will look for changes normally we use e.target.value to get input data for our use case we just need the current value which was typed so we are using native event data
let v = e.nativeEvent["data"]
: Retrieves the entered digit.let value = parseInt(v)
: Parses the digit to an integer. This ensures that only numeric values are processed.To Update the state:
!isNaN(value)
: Checks if it is a number or not such that an update can happen only if a number is entered.pass[i] = value
: Updates the PIN array at the current index with the entered digit.setPassword(pass)
: Updates the component's state with the modified PIN array.Moving Focus to the Next Input:
setActiveInput(i + 1)
: Sets the active input index to the next one.let nextInput = inpRefs?.current?.[i + 1]
: Retrieves the reference to the next input element.nextInput?.focus()
: Sets focus on the next input element.Note: Once it is at the last input, It will jump its focus to the button which is the next element in the form
According to my project requirements, I wanted input focus to shift to previous elements when backspace is pressed so to detect it I have used onKeyDown to capture backspace so when backspace is pressed it will shift focus to the previous element and when it is on the first digit input it will call the blur event.
Congratulations! You've successfully built a simple PIN entry component in NEXT JS. Now for the challenge, you can add a visibility switch for the entered pin.
I hope you found this tutorial helpful. Happy coding!