Hey Programmers! In this post, we will make the OTP Input Field with pure HTML, CSS & JavaScript. OTP Input fields are often used in the web application when you have to authenticate the user with a mobile number or email.
We will explore topics like DOM manipulation, event handling, and form validation also we will understand how to handle user input dynamically.
Also Read: Cool Email Input Feild With HTML & CSS
We will understand important concepts used in the code step by step. At the end of this blog post, I will share the entire source code so that you can run this project on your local device.
<div class="relative font-inter antialiased">
: This is the outermost container that applies some basic styles like font and anti-aliasing for smooth edges.<main class="main-container">
: The main section of the page containing the form and its related content.<div class="content-container">
: Centers the content within the main container.<div class="form-wrapper">
: Wraps the form content for centering and styling purposes.<div class="form-content">
: Contains the form header, form, and additional text.<header class="form-header">
: Includes the title and subtitle of the form.<form id="otp-form">
: The form for the OTP inputs and verification button.<div class="input-group">
: Groups the OTP input fields.<input type="text" class="otp-input" pattern="\d*" maxlength="1" />
: Four input fields for the OTP, each accepting one digit.<div class="button-container">
: Contains the verification button.<button type="submit" class="verify-button">Verify Account</button>
: Button to submit the form.<div class="resend-text">
: Text for the resend link.<footer class="page-footer">
: Footer containing a link to the source.<div class="banner" x-data="{ bannerOpen: true }">
: A banner with additional links and a close button.main-container
: Centers the main content vertically and horizontally with a full-height view and a light background.content-container
: Sets a maximum width and centers the content within the container.form-wrapper
: Centers the form horizontally.form-content
: Styles the form with padding, background color, and a shadow for a card-like appearance.form-header
: Adds spacing below the header elements.input-group
: Aligns the OTP inputs horizontally with spacing between them.otp-input
: Styles each input to be large, centered, and easy to read. The focus state highlights the active input.button-container
: Centers the submit button below the inputs.verify-button
: Styles the button with padding, color transitions, and a shadow.resend-text
: Styles the text and link for resending the OTP.page-footer
: Fixes the footer at the bottom with a link.banner
: Adds a fixed banner at the bottom with links and a close button.document.addEventListener('DOMContentLoaded', () => {...})
: Ensures the script runs after the DOM has fully loaded.const form = document.getElementById('otp-form')
: Gets the form element.const inputs = [...form.querySelectorAll('input[type=text]')]
: Selects all text input fields within the form.const submit = form.querySelector('button[type=submit]')
: Selects the submit button.handleKeyDown
: Prevents non-numeric characters, handles backspace/delete for moving focus.handleInput
: Moves focus to the next input or the submit button when a digit is entered.handleFocus
: Selects the text in the input when it gains focus.handlePaste
: Allows pasting of a 4-digit code directly into the inputs.inputs.forEach((input) => {...})
: Adds event listeners to each input for handling input, keydown, focus, and paste events.HTML:
<div class="relative font-inter antialiased">
<main class="main-container">
<div class="content-container">
<div class="form-wrapper">
<div class="form-content">
<header class="form-header">
<h1 class="form-title">Mobile Phone Verification</h1>
<p class="form-subtitle">Enter the 4-digit verification code that was sent to your phone number.</p>
</header>
<form id="otp-form">
<div class="input-group">
<input type="text" class="otp-input" pattern="\d*" maxlength="1" />
<input type="text" class="otp-input" maxlength="1" />
<input type="text" class="otp-input" maxlength="1" />
<input type="text" class="otp-input" maxlength="1" />
</div>
<div class="button-container">
<button type="submit" class="verify-button">Verify Account</button>
</div>
</form>
<div class="resend-text">Didn't receive code? <a class="resend-link" href="#0">Resend</a></div>
</div>
</div>
</div>
</main>
<footer class="page-footer">
<a class="footer-link" href="https://cruip.com">©Cruip - Tailwind CSS templates</a>
</footer>
<div class="banner" x-data="{ bannerOpen: true }">
<div class="banner-content">
<div class="banner-links">
<a class="banner-link" href="https://cruip.com/otp-form-example-made-with-tailwind-css-and-javascript/" target="_blank">Read Tutorial</a>
<span class="banner-separator">or</span>
<a class="download-link" href="https://github.com/cruip/cruip-tutorials/blob/main/otp-form/index.html" target="_blank" rel="noreferrer">
<span>Download</span>
<svg class="download-icon" xmlns="http://www.w3.org/2000/svg" width="9" height="9">
<path d="m1.649 8.514-.91-.915 5.514-5.523H2.027l.01-1.258h6.388v6.394H7.158l.01-4.226z" />
</svg>
</a>
</div>
<button class="close-banner" @click="bannerOpen = false">
<span class="sr-only">Close</span>
<svg class="close-icon" viewBox="0 0 16 16">
<path d="M12.72 3.293a1 1 0 00-1.415 0L8.012 6.586 4.72 3.293a1 1 0 00-1.414 1.414L6.598 8l-3.293 3.293a1 1 0 101.414 1.414l3.293-3.293 3.293 3.293a1 1 0 001.414-1.414L9.426 8l3.293-3.293a1 1 0 000-1.414z" />
</svg>
</button>
</div>
</div>
</div>
CSS:
<style>
/* Main container styles */
.main-container {
position: relative;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
background-color: #f8fafc;
overflow: hidden;
}
/* Content container */
.content-container {
width: 100%;
max-width: 96rem;
margin: 0 auto;
padding: 6rem 1rem;
padding-left: 1.5rem;
padding-right: 1.5rem;
}
/* Form wrapper */
.form-wrapper {
display: flex;
justify-content: center;
}
/* Form content */
.form-content {
max-width: 32rem;
margin: 0 auto;
text-align: center;
background-color: #fff;
padding: 2.5rem 1rem;
padding-left: 2rem;
padding-right: 2rem;
border-radius: 0.75rem;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
}
/* Form header */
.form-header {
margin-bottom: 2rem;
}
/* Form title */
.form-title {
font-size: 2rem;
font-weight: bold;
margin-bottom: 0.25rem;
}
/* Form subtitle */
.form-subtitle {
font-size: 0.9375rem;
color: #64748b;
}
/* Input group */
.input-group {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
}
/* OTP input */
.otp-input {
width: 3.5rem;
height: 3.5rem;
text-align: center;
font-size: 2rem;
font-weight: 800;
color: #0f172a;
background-color: #f1f5f9;
border: none;
border-radius: 0.375rem;
padding: 1rem;
outline: none;
appearance: none;
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}
.otp-input:hover {
border: 1px solid #e2e8f0;
}
.otp-input:focus {
background-color: #fff;
border: 1px solid #6366f1;
box-shadow: 0px 0px 0px 2px rgba(99, 102, 241, 0.25);
}
/* Button container */
.button-container {
max-width: 16.25rem;
margin: 1rem auto 0;
}
/* Verify button */
.verify-button {
width: 100%;
display: inline-flex;
justify-content: center;
white-space: nowrap;
border-radius: 0.5rem;
background-color: #6366f1;
padding: 0.625rem 0.875rem;
font-size: 0.875rem;
font-weight: 500;
color: #fff;
box-shadow: 0px 2px 4px rgba(99, 102, 241, 0.1);
transition: background-color 0.15s ease-in-out;
outline: none;
}
.verify-button:hover {
background-color: #4f46e5;
}
.verify-button:focus {
outline: none;
box-shadow: 0px 0px 0px 2px rgba(99, 102, 241, 0.3);
}
/* Resend text */
.resend-text {
font-size: 0.875rem;
color: #64748b;
margin-top: 1rem;
}
.resend-link {
font-weight: 500;
color: #6366f1;
text-decoration: none;
transition: color 0.15s ease-in-out;
}
.resend-link:hover {
color: #4f46e5;
}
/* Page footer */
.page-footer {
position: absolute;
left: 1.5rem;
right: 1.5rem;
bottom: 1rem;
text-align: center;
}
.page-footer a {
font-size: 0.75rem;
color: #64748b;
text-decoration: none;
}
.page-footer a:hover {
text-decoration: underline;
}
/* Banner */
.banner {
position: fixed;
bottom: 0;
right: 0;
width: 100%;
max-width: auto;
z-index: 50;
padding: 0 1.5rem;
}
.banner-content {
background-color: #1e293b;
font-size: 0.875rem;
padding: 0.75rem;
border-radius: 0.375rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
}
/* Banner links */
.banner-links {
display: inline-flex;
align-items: center;
color: #94a3b8;
}
.banner-link {
font-weight: 500;
color: #cbd5e1;
text-decoration: none;
}
.banner-link:hover {
text-decoration: underline;
}
.banner-separator {
font-style: italic;
padding: 0 0.375rem;
}
.download-link {
font-weight: 500;
color: #6366f1;
display: flex;
align-items: center;
text-decoration: none;
}
.download-link span {
margin-right: 0.25rem;
}
.download-icon {
fill: #6366f1;
}
/* Close banner */
.close-banner {
color: #94a3b8;
display: flex;
align-items: center;
border-left: 1px solid #334155;
padding-left: 0.5rem;
margin-left: 0.75rem;
}
.close-banner:hover {
color: #e2e8f0;
}
.close-icon {
width: 1rem;
height: 1rem;
fill: currentColor;
}
</style>
JavaScript:
<script>
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('otp-form')
const inputs = [...form.querySelectorAll('input[type=text]')]
const submit = form.querySelector('button[type=submit]')
const handleKeyDown = (e) => {
if (
!/^[0-9]{1}$/.test(e.key)
&& e.key !== 'Backspace'
&& e.key !== 'Delete'
&& e.key !== 'Tab'
&& !e.metaKey
) {
e.preventDefault()
}
if (e.key === 'Delete' || e.key === 'Backspace') {
const index = inputs.indexOf(e.target);
if (index > 0) {
inputs[index - 1].value = '';
inputs[index - 1].focus();
}
}
}
const handleInput = (e) => {
const { target } = e
const index = inputs.indexOf(target)
if (target.value) {
if (index < inputs.length - 1) {
inputs[index + 1].focus()
} else {
submit.focus()
}
}
}
const handleFocus = (e) => {
e.target.select()
}
const handlePaste = (e) => {
e.preventDefault()
const text = e.clipboardData.getData('text')
if (!new RegExp(`^[0-9]{${inputs.length}}$`).test(text)) {
return
}
const digits = text.split('')
inputs.forEach((input, index) => input.value = digits[index])
submit.focus()
}
inputs.forEach((input) => {
input.addEventListener('input', handleInput)
input.addEventListener('keydown', handleKeyDown)
input.addEventListener('focus', handleFocus)
input.addEventListener('paste', handlePaste)
})
})
</script>
Last Updated: June 18, 2024