Please input your OTP code - A how-to guide
How to create an OTP input component with Vue.js
Introduction
As part of the authentication process, many products require users to provide a one-time password (OTP). By way of design, the inputs for this OTP are usually single-input boxes that allow only one digit per input. By the end of this article, you will have created a custom, reusable OTP input component with Vue.js. We will walk through creating the base template with Vue, styling, validations and then adding extra functionality such as copy and paste.
Prerequisites
To be able to follow this article with minimal difficulty, you will need to have basic knowledge of HTML, CSS, Javascript and Vue.js
Creating the base template
Firstly, we will create a Vue file in the components folder and create the base template for the OTP input component.
<template>
<main>
<div class="container" ref="container">
<input
v-for="(digitInput, index) in otpLength"
:key="index"
ref="otpInput"
v-model="otpArray[index]"
@keydown="handleEnter(index, $event)"
@input="handleInput(index, $event)"
@paste="handlePaste(index, $event)"
type="text"
step="1"
maxlength="1"
class="input"
/>
</div>
</main>
</template>
<script>
export default {
data() {
return {
otpArray: [],
error: '',
joinedValue: "",
}
},
props: {
otpLength: {
type: Number,
default: 6
}
},
}
</script>
<style>
</style>
In this code snippet, we are using a for-loop to control the amount of input boxes. By using a for-loop like this, we can dynamically set the number of input boxes we want by using props. If no otpLength prop is provided, the default of 6 would be used.
By adding a maxlength
of 1 to the input, we are ensuring that only one digit is entered into each input. We have also added a ref to the parent of the inputs, this way we can access the HTML in the script tag and add some more functionalities via methods.
To use the OTP input component in the parent class, we will import it and define it in the component object. In my case, the parent is the App.vue file.
<template>
<div id="app">
<otp-input @otp-complete="handleCode"></otp-input>
</div>
</template>
<script>
import OtpInput from './components/otp.vue'
export default {
name: 'App',
components: {
OtpInput
},
data(){
return {
otpCode: '',
}
},
methods: {
handleCode(otpCode){
this.otpCode = otpCode
console.log(otpCode)
}
}
}
</script>
<style>
</style>
Adding styling
To make the OTP component easier on the eyes, we will add some basic styling to the styles
tag
main {
display: flex;
flex-direction: column;
justify-content: center;
height: 80vh;
}
.container {
display: flex;
gap: 1rem;
margin: 1rem auto;
width: 80%;
justify-content: center;
}
.input {
outline: none;
width: 2rem;
display: flex;
justify-content: center;
padding: 0px;
margin-right: 1rem;
color: inherit;
border: 1px solid #222222;
border-radius: 8px;
padding: 0.85rem;
padding-right: 0;
font-family: inherit;
font-size: 1.5rem;
font-weight: 700;
line-height: 20px;
appearance: none;
letter-spacing: 18px;
}
.input:focus {
border: 2px solid #222222;
}
After adding the styles, the component should be looking like this:
Controlling the inputs
Most OTP inputs accept only numeric inputs. Therefore, we will be adding validations to ensure that users can only enter numeric values into the fields. If your use case does not require validations, you can skip this step.
We will start by adding a method to the script tag that will handle the validations on key down and on input. We already added the key down and input event listeners to the input and bound them to the handleEnter
and handleInput
functions respectively. In the methods
object of the component, we will now write these functions.
methods: {
handleEnter(index, event) {
const keypressed = event.keyCode || event.which;
if((keypressed < 48 || keypressed > 57) && keypressed !== 8 && !event.ctrlKey){
event.preventDefault();
}
if (keypressed === 8 || keypressed === 46) {
event.preventDefault();
if (!this.otpArray[index] && index > 0) {
this.otpArray[index - 1] = "";
this.$refs.otpInput[index - 1].value = "";
this.$refs.otpInput[Math.max(0, index - 1)].focus();
} else {
this.otpArray[index] = "";
this.$refs.otpInput[index].value = "";
}
}
},
handleInput(index, event) {
if (index < this.otpLength - 1) {
this.$refs.otpInput[index + 1].focus();
}
if (index === this.otpLength - 1) {
this.$refs.otpInput[index].blur();
}
this.joinedValue = this.otpArray.join("");
if (this.joinedValue.length === this.otpLength) {
this.$emit("otp-complete", this.joinedValue);
}
},
},
The handleEnter
function uses the keycode to know which keyboard key was pressed. If a key that is not a number key i.e. 0 - 9 is pressed, the default behavior is prevented. This means nothing happens to the input when a non-numeric number is pressed.
The next thing is to handle backspace and delete keys. The approach taken is the clear the current focused input if it contains a value when the backspace key is pressed. If it is empty, however, clear the preceding input and move the focus to the preceding input.
In the handleInput
function, we move the focus to the next input. If the focus is on the last input already, we blur. Because we are using one input for one digit, we still need to track the complete OTP as one 6-digit value. We do this by using .join
on the otpArray on each input. When the OTP is complete, i.e. all inputs are filled, we emit a variable that can be used in the parent to access the joined OTP value.
Handling paste events
To handle paste events, we will start by creating a method that will handle the functionality and then we will call this method in the handleInput method when the event input type is insertFromPaste
handlePasteEvent(index){
navigator.clipboard.readText().then((pastedText) => {
const shavedText = pastedText.replace(/[^0-9]/g, '')
for (let i = 0; i < shavedText.length && index + i < this.otpLength; i++) {
this.otpArray[index + i] = shavedText.charAt(i);
if (index + i + 1 < this.otpLength) {
this.$refs.otpInput[index + i + 1].dispatchEvent(
new Event("input")
);
}
}
});
},
The handlePasteEvent
method gets the copied text from the clipboard. Because we are creating a numeric-only OTP input, we use regex to replace all non-numeric characters in the copied text with an empty string. You can ignore this step and use pastedText in the for-loop directly if your use case allows for alphanumeric characters. In the for-loop, we append each digit to the correct index in the otpArray. We also dispatch an input event to the next input which simulates user input and triggers the input field to accept the pasted character.
The updated handleInput
method is now:
handleInput(index, event) {
if (index < this.otpLength - 1) {
this.$refs.otpInput[index + 1].focus();
}
if (index === this.otpLength - 1) {
this.$refs.otpInput[index].blur();
}
if (event.inputType === "insertFromPaste") {
this.handlePasteEvent(index)
}
this.joinedValue = this.otpArray.join("");
if (this.joinedValue.length === this.otpLength) {
this.$emit("otp-complete", this.joinedValue);
}
},
This will take care of pasting using Ctrl+V. To take care of pasting that involves right-clicking and pasting, we need to call the function on the paste
event. The handlePaste
method that we used in our input will do this for us.
handlePaste(index, event) {
this.handlePasteEvent(index)
this.$emit("paste", event);
}
With this, the paste should work as expected.
Here is our component in action
Conclusion
In this article, we have learned how to build our own custom OTP input component in Vue. We have added styles as well as paste functionality. This is a component that you can use and extend in any project that requires users to provide some sort of code or OTP. The OTP code can then be used in a parent component through the on-complete
event to perform any checks that you will need to perform for your use case. The code for this project can be found here: on my GitHub. Thanks for reading.