One-Time Password (OTP) forms are widely used for two-factor authentication (2FA), account verification, and secure transactions. Adding a countdown timer to an OTP form enhances user experience by creating urgency and ensuring the OTP expires after a set period, bolstering security. This article provides a step-by-step guide to building a modern OTP form with a countdown timer using HTML, CSS, and JavaScript. We’ll cover the design, functionality, best practices, and tools to assist in development, along with a complete code example and references to free AI tools for prototyping and testing.
An OTP form with a countdown timer serves multiple purposes:
This guide assumes basic knowledge of HTML, CSS, and JavaScript. However, we’ll break down each component to make it accessible for beginners and intermediate developers alike.
The HTML structure will include a form for OTP input, a display for the countdown timer, and buttons for submitting or resending the OTP. We’ll use semantic HTML for accessibility and SEO.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OTP Verification Form</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="otp-container">
<h2>OTP Verification</h2>
<p>Enter the 6-digit code sent to your email or phone.</p>
<form id="otp-form">
<div class="otp-inputs">
<input type="text" maxlength="1" required>
<input type="text" maxlength="1" required>
<input type="text" maxlength="1" required>
<input type="text" maxlength="1" required>
<input type="text" maxlength="1" required>
<input type="text" maxlength="1" required>
</div>
<div class="timer">
<span id="countdown">00:30</span>
</div>
<button type="submit">Verify OTP</button>
<button type="button" id="resend-btn" disabled>Resend OTP</button>
</form>
<p id="message"></p>
</div>
<script src="script.js"></script>
</body>
</html>
The CSS will make the form visually appealing, responsive, and user-friendly. We’ll use Tailwind CSS (via CDN) for rapid styling, combined with custom CSS for specific elements.
/* styles.css */
body {
font-family: 'Arial', sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f4f8;
margin: 0;
}
.otp-container {
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
text-align: center;
max-width: 400px;
width: 100%;
}
h2 {
color: #333;
margin-bottom: 1rem;
}
p {
color: #666;
margin-bottom: 1.5rem;
}
.otp-inputs {
display: flex;
gap: 10px;
justify-content: center;
margin-bottom: 1.5rem;
}
.otp-inputs input {
width: 40px;
height: 40px;
text-align: center;
font-size: 1.2rem;
border: 2px solid #ddd;
border-radius: 5px;
outline: none;
transition: border-color 0.3s;
}
.otp-inputs input:focus {
border-color: #007bff;
}
.timer {
margin-bottom: 1.5rem;
font-size: 1.1rem;
color: #ff4d4f;
}
button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
margin: 0.5rem;
}
button[type="submit"] {
background-color: #007bff;
color: white;
}
button[type="button"] {
background-color: #ccc;
color: #333;
}
button[type="button"]:not(:disabled) {
background-color: #28a745;
color: white;
}
#message {
color: #ff4d4f;
margin-top: 1rem;
}
@media (max-width: 480px) {
.otp-container {
padding: 1rem;
}
.otp-inputs input {
width: 35px;
height: 35px;
}
}
To use Tailwind CSS, include this in the <head>
of your HTML:
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
The JavaScript handles the OTP input logic, countdown timer, and resend functionality. The timer will count down from 30 seconds, and the resend button will activate once the timer expires.
// script.js
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('otp-form');
const inputs = document.querySelectorAll('.otp-inputs input');
const resendBtn = document.getElementById('resend-btn');
const countdownEl = document.getElementById('countdown');
const messageEl = document.getElementById('message');
let timeLeft = 30;
let timer;
// Start countdown
function startCountdown() {
resendBtn.disabled = true;
timeLeft = 30;
countdownEl.textContent = `00:${timeLeft.toString().padStart(2, '0')}`;
timer = setInterval(() => {
timeLeft--;
countdownEl.textContent = `00:${timeLeft.toString().padStart(2, '0')}`;
if (timeLeft <= 0) {
clearInterval(timer);
resendBtn.disabled = false;
messageEl.textContent = 'OTP expired. Please resend.';
}
}, 1000);
}
// Handle input navigation
inputs.forEach((input, index) => {
input.addEventListener('input', () => {
if (input.value.length === 1 && index < inputs.length - 1) {
inputs[index + 1].focus();
}
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && input.value === '' && index > 0) {
inputs[index - 1].focus();
}
});
});
// Form submission
form.addEventListener('submit', (e) => {
e.preventDefault();
const otp = Array.from(inputs).map(input => input.value).join('');
if (otp.length === 6 && timeLeft > 0) {
messageEl.textContent = 'OTP Verified Successfully!';
messageEl.style.color = '#28a745';
clearInterval(timer);
} else {
messageEl.textContent = 'Invalid or expired OTP.';
}
});
// Resend OTP
resendBtn.addEventListener('click', () => {
messageEl.textContent = 'New OTP sent!';
messageEl.style.color = '#007bff';
inputs.forEach(input => (input.value = ''));
inputs[0].focus();
startCountdown();
});
// Initialize countdown
startCountdown();
});
To streamline development, you can use free AI tools for prototyping and testing:
For AI-powered assistance:
To make your OTP form stand out, consider these enhancements:
Below is the complete code combining HTML, CSS, and JavaScript. Save these as index.html
, styles.css
, and script.js
in the same directory, then open index.html
in a browser.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OTP Verification Form</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<style>
body {
font-family: 'Arial', sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f4f8;
margin: 0;
}
.otp-container {
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
text-align: center;
max-width: 400px;
width: 100%;
}
h2 {
color: #333;
margin-bottom: 1rem;
}
p {
color: #666;
margin-bottom: 1.5rem;
}
.otp-inputs {
display: flex;
gap: 10px;
justify-content: center;
margin-bottom: 1.5rem;
}
.otp-inputs input {
width: 40px;
height: 40px;
text-align: center;
font-size: 1.2rem;
border: 2px solid #ddd;
border-radius: 5px;
outline: none;
transition: border-color 0.3s;
}
.otp-inputs input:focus {
border-color: #007bff;
}
.timer {
margin-bottom: 1.5rem;
font-size: 1.1rem;
color: #ff4d4f;
}
button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
margin: 0.5rem;
}
button[type="submit"] {
background-color: #007bff;
color: white;
}
button[type="button"] {
background-color: #ccc;
color: #333;
}
button[type="button"]:not(:disabled) {
background-color: #28a745;
color: white;
}
#message {
color: #ff4d4f;
margin-top: 1rem;
}
.shake {
animation: shake 0.5s;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25%, 75% { transform: translateX(-5px); }
50% { transform: translateX(5px); }
}
@media (max-width: 480px) {
.otp-container {
padding: 1rem;
}
.otp-inputs input {
width: 35px;
height: 35px;
}
}
</style>
</head>
<body>
<div class="otp-container">
<h2>OTP Verification</h2>
<p>Enter the 6-digit code sent to your email or phone.</p>
<form id="otp-form">
<div class="otp-inputs">
<input type="text" maxlength="1" required aria-label="OTP digit 1">
<input type="text" maxlength="1" required aria-label="OTP digit 2">
<input type="text" maxlength="1" required aria-label="OTP digit 3">
<input type="text" maxlength="1" required aria-label="OTP digit 4">
<input type="text" maxlength="1" required aria-label="OTP digit 5">
<input type="text" maxlength="1" required aria-label="OTP digit 6">
</div>
<div class="timer">
<span id="countdown">00:30</span>
</div>
<button type="submit">Verify OTP</button>
<button type="button" id="resend-btn" disabled>Resend OTP</button>
</form>
<p id="message"></p>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('otp-form');
const inputs = document.querySelectorAll('.otp-inputs input');
const resendBtn = document.getElementById('resend-btn');
const countdownEl = document.getElementById('countdown');
const messageEl = document.getElementById('message');
let timeLeft = 30;
let timer;
function startCountdown() {
resendBtn.disabled = true;
timeLeft = 30;
countdownEl.textContent = `00:${timeLeft.toString().padStart(2, '0')}`;
timer = setInterval(() => {
timeLeft--;
countdownEl.textContent = `00:${timeLeft.toString().padStart(2, '0')}`;
if (timeLeft <= 0) {
clearInterval(timer);
resendBtn.disabled = false;
messageEl.textContent = 'OTP expired. Please resend.';
}
}, 1000);
}
inputs.forEach((input, index) => {
input.addEventListener('input', () => {
input.value = input.value.replace(/[^0-9]/g, '');
if (input.value.length === 1 && index < inputs.length - 1) {
inputs[index + 1].focus();
}
if (index === inputs.length - 1 && input.value.length === 1) {
form.dispatchEvent(new Event('submit'));
}
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && input.value === '' && index > 0) {
inputs[index - 1].focus();
}
});
input.addEventListener('paste', (e) => {
if (index === 0) {
const paste = e.clipboardData.getData('text').replace(/[^0-9]/g, '');
if (paste.length === 6) {
inputs.forEach((input, i) => (input.value = paste[i]));
form.dispatchEvent(new Event('submit'));
}
}
});
});
form.addEventListener('submit', (e) => {
e.preventDefault();
const otp = Array.from(inputs).map(input => input.value).join('');
if (otp.length === 6 && timeLeft > 0) {
messageEl.textContent = 'OTP Verified Successfully!';
messageEl.style.color = '#28a745';
clearInterval(timer);
} else {
messageEl.textContent = 'Invalid or expired OTP.';
form.classList.add('shake');
setTimeout(() => form.classList.remove('shake'), 500);
}
});
resendBtn.addEventListener('click', () => {
messageEl.textContent = 'New OTP sent!';
messageEl.style.color = '#007bff';
inputs.forEach(input => (input.value = ''));
inputs[0].focus();
startCountdown();
});
startCountdown();
});
</script>
</body>
</html>