May
25

Create an OTP Form with Countdown Timer

05/25/2025 12:00 AM by Admin in Html


otp verification

 

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.

Why Use an OTP Form with a Countdown Timer?

An OTP form with a countdown timer serves multiple purposes:

  • Enhanced Security: OTPs expire after a short period, reducing the risk of unauthorized access.
  • Improved User Experience: A timer informs users how long they have to enter the code, creating a sense of urgency.
  • Modern Design: A sleek, responsive design ensures compatibility across devices, from desktops to smartphones.
  • Versatility: OTP forms are used in banking, e-commerce, account recovery, and more.

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.

Step-by-Step Guide to Building the OTP Form

1. Setting Up the HTML Structure

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>

2. Styling with CSS

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">

3. JavaScript for OTP Logic and Countdown Timer

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();
});

4. Best Practices for OTP Forms

  • Input Restrictions: Limit each input to one character and allow only numeric values.
  • Accessibility: Use ARIA attributes and ensure keyboard navigation.
  • Responsive Design: Test on multiple devices to ensure usability.
  • Security: In production, validate OTPs server-side and use HTTPS.
  • User Feedback: Provide clear messages for success, failure, or expiration.

5. Testing and Prototyping with Free AI Tools

To streamline development, you can use free AI tools for prototyping and testing:

For AI-powered assistance:

6. Enhancing the OTP Form

To make your OTP form stand out, consider these enhancements:

  • Auto-Submit: Automatically submit the form once all inputs are filled.
  • Paste Support: Allow users to paste a full OTP code and distribute it across inputs.
  • Animations: Add subtle animations for input focus or timer updates using CSS or libraries like GSAP (https://greensock.com/gsap).
  • Localization: Support multiple languages for global accessibility.

Complete Code Example

Below is the complete code combining HTML, CSS, and JavaScript. Save these as index.htmlstyles.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>

leave a comment
Please post your comments here.