How to create OTP input using Alpine.js and Tailwind CSS cover image

How to create OTP input using Alpine.js and Tailwind CSS

Rajesh Dewle | 29 Dec 2020

Introduction

Alpine.js minimal library created by Caleb Porzio allows us to write JavaScript as a reactive and declarative code as an inline HTML. It's a Vue.js template flavoured. Target is to fill in the gap jQuery and major frameworks like Vue.js and React. Not meant to replace framework like Vue.js or react. Use case is for when minimal JavaScript is a needed like drop-downs, tabs, modals etc. and it's great for server-rendered apps like Laravel or Rails where you just a need to toggle some JavaScript components.

In this tutorial, we are going to create a simple OTP/Pin input using Alpine.js and Tailwind CSS. Here we create multiple inputs to enter OTP/Pin. You can paste them or enter manually if you want.

Installation.

First, we need to create index.html file in OTP folder. Now just insert the CDN bellow to the end of the <head> section of your HTML file. Or you can install it with NPM as well.

<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.7.3/dist/alpine.min.js" defer></script>

We also need Tailwind CSS which I use below CDN. Otherwise installing Tailwind CSS can be a slightly different process depending on what other frameworks/tools you're using. check it out Tailwind CSS installation. Add the following CDN to the <head> tag.

<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">

Now our index.html code should look something like this and the page should be blank.

File: OTP/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>OTP/Pin Number Module</title>

    <!-- Tailwind CSS -->
    <link
      href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"
      rel="stylesheet"
    />
    <!-- Alpine.js -->
    <script
      src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.7.3/dist/alpine.min.js"
      defer
    ></script>
  </head>
  <body>
    <div>
      <!-- Content goes here -->
    </div>
  </body>
</html>

Create a simple form

Now we need to create a form.

File: OTP/index.html

<!-- ... -->
<body>
  <div class="py-6 px-6 w-80 border mx-auto text-center my-6">
    <form action="#" x-data="otpForm()" method="POST">
      <!-- ... -->
    </form>
  </div>
  <script>
    function otpForm() {
      return {
        length: 6,
        value: "",
      };
    }
  </script>
</body>
<!-- ... -->

The first step to using Alpine is to define a state. The state goes wherever it is needed and has the same scope as the HTML selector you put in. In the code above, we declare scope using x-data directive by passing with the function otpForm and set the value of the state. And added some Tailwind CSS classes for styles.

Create a input fields.

In the above code, we define length with default value 6. You can change as per your input field. Because here we are going to use a for-each loop and that depends on this length. In alpine js to use for each loop, we need to use the 'x-for' directive. And create new DOM nodes for each item in an array. Needs to be used on a <template> tag.

File: OTP/index.html

<!-- ... -->
<form action="#" x-data="otpForm()" method="POST">
  <div class="flex justify-between">
    <template x-for="(input, index) in length" :key="index">
      <input
        type="tel"
        maxlength="1"
        class="border border-gray-500 w-10 h-10 text-center"
      />
    </template>
  </div>
</form>
<!-- ... -->

Refresh your browser page and you can see 6 input fields. Here we want to add a hidden input field to store OTP/Pin value. Add below code before closing the form tag.

<input type="hidden" name="otp" x-model="value">

Add x-model directive for two-way data binding to an element. After the hidden input field we are going to add a ‘verify’ button to submit the form.

<button type="submit" class="btn-primary mx-auto block bg-gray-500 w-full p-2 mt-2 text-white">Verify OTP!</button>

Now your form code should look something like this and the page should be your form with 6 input fields.

File: OTP/index.html

<!-- ... -->
<form action="#" x-data="otpForm()" method="POST">
  <div class="flex justify-between">
    <template x-for="(input, index) in length" :key="index">
      <input
        type="tel"
        maxlength="1"
        class="border border-gray-500 w-10 h-10 text-center"
      />
    </template>
  </div>
  <input type="hidden" name="otp" x-model="value" />
  <button
    type="submit"
    class="btn-primary mx-auto block bg-gray-500 w-full p-2 mt-2 text-white"
  >
    Verify OTP!
  </button>
</form>
<!-- ... -->

Add some logic

We want to handle input event, paste event and backspace key event.

Handle Inputs: Now time to handle input event, for that we need to add x-on:input event listener to input fields with handleInput($event) method and pass the event to this method. also x-ref directive to retrieve DOM elements marked with x-ref inside the component with index value.

File: OTP/index.html

<!-- ... -->
<input
  type="tel"
  maxlength="1"
  class="border border-gray-500 w-10 h-10 text-center"
  :x-ref="index"
  x-on:input="handleInput($event)"
/>
<!-- ... -->

And in our otpForm() function we need to add handleInput method with some logic.

File: OTP/index.html

return {
  // ...
  handleInput(e) {
    const input = e.target;

    this.value = Array.from(Array(this.length), (element, i) => {
      return this.$refs[i].value || "";
    }).join("");

    if (input.nextElementSibling && input.value) {
      input.nextElementSibling.focus();
      input.nextElementSibling.select();
    }
  },
};

In the above code, we added handleInput method, When input in the input field its trigger and check the value length and set the event value to state value. After that, if condition checks the nextElementSibling and input value in available that time focus and select on the next input filed.

Handle Paste: Now we handling paste event. for that, we need to add x-on:paste event listener to input fields with handlePaste($event) method and pass the event to this method.

File: OTP/index.html

<!-- ... -->
<input
  type="tel"
  maxlength="1"
  class="border border-gray-500 w-10 h-10 text-center"
  :x-ref="index"
  x-on:input="handleInput($event)"
  x-on:paste="handlePaste($event)"
/>
<!-- ... -->

And in our otpForm() function we need to add handlePaste method with some logic.

File: OTP/index.html

return {
  // ...
  handlePaste(e) {
    const paste = e.clipboardData.getData("text");
    this.value = paste;

    const inputs = Array.from(Array(this.length));

    inputs.forEach((element, i) => {
      this.$refs[i].value = paste[i] || "";
    });
  },
};

In the above code, we added the handlePaste method, when we paste the OTP/Pin it's a trigger and get the event. first, it's getting the value from the clipboardData as a text and store them in a paste variable. Then assign this value to value state. After that create a simple array with the length that we declare in the state. And assign the value to each element index that is marked with 'x-ref' by using forEach loop.

Handle Backspace: Now we need to handle the backspace key down event. For that we need to add @keydown.backspace event listener to the input field with handleBackspace method with the passing index value.

File: OTP/index.html

<!-- ... -->
<input
  type="tel"
  maxlength="1"
  class="border border-gray-500 w-10 h-10 text-center"
  :x-ref="index"
  x-on:input="handleInput($event)"
  x-on:paste="handlePaste($event)"
  x-on:keydown.backspace="$event.target.value || handleBackspace($event.target.getAttribute('x-ref'))"
/>
<!-- ... -->

In the above code we added keydown.backspace event, Here we trigger the event value or handleBackspace method. And in our handleBackspace method, we need to add the following logic.

File: OTP/index.html

return {
  // ...
  handleBackspace(e) {
    const previous = parseInt(e, 10) - 1;
    this.$refs[previous] && this.$refs[previous].focus();
  },
};

In the above code, we added handleBackspace method. When we press the backspace button it's a trigger and convert them in integer and assign to previous variable with decreasing with 1 then focus the previous input field.

Code available on github: https://github.com/rajeshdewle/otp-pin-using-alpine-js-and-tailwindcss