webdevpuneet.com

Bootstrap OTP Login Form — Free HTML CSS JS Snippet

Bootstrap OTP Login Form · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

Copy & paste

Source Code

<div class="container py-5 d-flex justify-content-center">
  <div class="card bsotp-card">
    <div class="card-body p-4">

      <div id="bsotpStep1">
        <h5 class="fw-bold mb-1 text-center">Log in</h5>
        <p class="text-muted small mb-3 text-center">Enter your email or phone to receive a one-time code.</p>
        <label class="form-label small fw-semibold" for="bsotpContact">Email or phone</label>
        <input type="text" class="form-control mb-3" id="bsotpContact" placeholder="[email protected]">
        <div class="small text-danger mb-2 d-none" id="bsotpContactError">Enter a valid email or phone number.</div>
        <button class="btn btn-dark w-100 fw-bold" id="bsotpSendBtn">Send code</button>
      </div>

      <div id="bsotpStep2" class="d-none">
        <h5 class="fw-bold mb-1 text-center">Enter the code</h5>
        <p class="text-muted small mb-3 text-center">We sent a 6-digit code to <span id="bsotpTarget" class="fw-semibold"></span>.</p>
        <div class="d-flex gap-2 justify-content-center mb-3" id="bsotpDigits">
          <input type="text" inputmode="numeric" maxlength="1" class="form-control text-center bsotp-box">
          <input type="text" inputmode="numeric" maxlength="1" class="form-control text-center bsotp-box">
          <input type="text" inputmode="numeric" maxlength="1" class="form-control text-center bsotp-box">
          <input type="text" inputmode="numeric" maxlength="1" class="form-control text-center bsotp-box">
          <input type="text" inputmode="numeric" maxlength="1" class="form-control text-center bsotp-box">
          <input type="text" inputmode="numeric" maxlength="1" class="form-control text-center bsotp-box">
        </div>
        <p class="small text-center mb-3" id="bsotpStatus">&nbsp;</p>
        <button class="btn btn-dark w-100 fw-bold mb-2" id="bsotpVerifyBtn" disabled>Verify &amp; log in</button>
        <p class="small text-muted text-center mb-0">
          <a href="javascript:void(0)" id="bsotpBack">&larr; Change contact</a>
          &nbsp;&middot;&nbsp;
          <a href="javascript:void(0)" id="bsotpResend">Resend code</a>
          <span id="bsotpTimer" class="text-muted"></span>
        </p>
      </div>

    </div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Two-step card flow: contact entry, then OTP entry, swapped via Bootstrap d-none classes
Email or phone validation with a dedicated isValidContact() check and inline error state
Animated CSS fade-in transition when step two is revealed
Dynamic confirmation text showing the exact contact value the code was "sent" to
Six-box OTP input with auto-advance, backspace-to-previous, and full-code paste support
Live 30-second resend countdown that disables the Resend link and updates every second
Countdown is cleared with clearInterval when navigating back, preventing stale timers
Change-contact link returns to step one without losing the card layout

About this UI Snippet

Bootstrap OTP Login Form — HTML, CSS & JavaScript

Screenshot of the Bootstrap OTP Login Form snippet rendered live

This snippet models login as two distinct states inside one .card, not two pages: #bsotpStep1 collects a contact value in a Bootstrap form-control, and #bsotpStep2 — initially hidden with d-none — holds the six-box OTP entry. Clicking Send code runs isValidContact(), a small function that checks the typed value against either an email regex or a loose phone-number regex, so both formats are accepted. On success, revealStep2() toggles the d-none classes on both steps in one function, writes the entered contact into #bsotpTarget so the confirmation text is genuinely dynamic ("We sent a 6-digit code to [email protected]"), resets the six boxes, and focuses the first one. Because #bsotpStep2 carries a bsotpFadeIn keyframe animation triggered on d-none removal, the transition between steps reads as a reveal rather than an abrupt layout jump.

The six OTP boxes reuse the auto-advance / backspace-to-previous / full-paste-distribution pattern — typing a digit moves focus forward, Backspace on an empty box moves focus back, and a paste event listener spreads a multi-digit clipboard value across all six boxes starting from index 0. What's specific to this snippet is the resend flow: startCountdown() sets a 30-second RESEND_SECONDS timer, disables the Resend link by adding the disabled/text-muted classes and setting pointerEvents: 'none' (a class alone doesn't stop clicks on an <a>, so the inline style is required), and updates #bsotpTimer every second via setInterval with a live "(29s)" style countdown. When it reaches zero, clearInterval fires and the link's disabled styling and pointer-events lock are both removed, re-enabling it.

A pitfall this handles explicitly: switching back to step 1 via the "Change contact" link must stop the running countdown with clearInterval(countdownId) — otherwise a stale timer from a previous OTP request would keep ticking in the background and could re-enable a resend link that no longer exists in the visible step, or worse, leak an interval that never gets cleared across repeated back-and-forth navigation. Keeping countdownId in module scope and clearing it on every fresh startCountdown() call (not just on navigating back) also prevents two overlapping intervals from both writing to #bsotpTimer if Resend is somehow triggered twice in a row.

The contact validation itself deliberately accepts two very different shapes of input through one function rather than forcing the user to pick "email" or "phone" up front: isValidContact() runs the trimmed value through an email regex first, and only if that fails, through a looser phone regex accepting digits, spaces, parentheses, plus signs, and hyphens of at least seven characters. Returning true from either check is enough, so a login page doesn't need a separate toggle or tab for "email" vs "phone" login — the same input field and the same Send code button handle both, which mirrors how most real-world OTP login screens are built today. Because checkComplete() is called from within the shared input listener attached to every box in the same boxes.forEach loop that also handles digit-stripping and auto-advance, the Verify button's disabled state is always recalculated immediately after any keystroke, paste, or programmatic reset, rather than relying on a separate validation pass triggered only on submit.

Step by step

How to Use

  1. 1
    Load the snippetA single-step card appears asking for an email or phone number, with a Send code button.
  2. 2
    Type an invalid value and click Send codeThe field outlines red and an inline error appears below it; nothing advances.
  3. 3
    Type a valid email and click Send codeThe card fades into step two, showing six empty digit boxes and your entered contact echoed back in the subtext.
  4. 4
    Type the code 135790Focus auto-advances between boxes as you type, and the Verify button enables once all six are filled.
  5. 5
    Watch the Resend linkIt appears grayed out with a live "(30s)" countdown next to it that ticks down every second until it re-enables itself.
  6. 6
    Click Verify & log inA green success message appears for the correct code, or a red error with all six boxes outlined invalid for any other code.

Real-world uses

Common Use Cases

Passwordless login and OTP-based authentication
A complete two-step login pattern, complementary to a traditional bootstrap-login-form-validation form for apps offering both options.
Generalizing the resend cooldown beyond a single OTP code
See bootstrap-rate-limited-action-button for the same anti-spam idea as a reusable sliding-window limiter for any repeatable action.
Giving a user a fallback if they lose access to their phone
Pair with bootstrap-two-factor-backup-codes so an OTP-only login flow always has a recovery path.
Multi-step account flows
The same reveal-and-countdown pattern fits a bootstrap-multistep-signup-modal-style signup wizard that needs phone verification mid-flow.
Mobile-first apps favoring OTP over passwords
Common in apps where phone-number login is the primary flow rather than a fallback.
Learning countdown timers and step transitions
A clear, self-contained example of setInterval-driven UI state combined with a two-step reveal animation.
This library's other verification pattern
Compare the six-box handling here with the always-visible variant in bootstrap-2fa-verification-form to see the two-step reveal difference.

Got questions?

Frequently Asked Questions

That snippet shows the six-digit boxes immediately as a single-step form. This one models the realistic two-step login: a contact field first, then an animated reveal of the OTP boxes only after a valid contact is submitted, plus a live resend countdown that the 2FA snippet does not include.

Yes. In React, replace the two d-none toggles with a step state variable and useEffect for the setInterval cleanup (clearInterval in the effect's return function); in Vue, use ref for the step and onUnmounted to clear the interval; in Angular, manage the interval in ngOnDestroy so it is not left running after the component is destroyed.

Yes — clicking "Change contact" calls clearInterval on the stored countdownId before returning to step one, and starting a new send always calls startCountdown() fresh, which itself clears any prior interval first, so no two timers ever run simultaneously.

The email check requires a basic [email protected] shape via regex, and the phone check accepts 7 or more characters made up of digits, spaces, parentheses, plus signs, and hyphens — loose enough for most international formats without pretending to be a full validation library.

Keep the two-step HTML structure and all JS untouched, then swap .card/.card-body and .form-control for Tailwind utilities like rounded-xl border p-6 and border rounded px-3 py-2, and replace the .bsotp-box sizing with Tailwind's w-11 h-13 text-center classes.

Yes, 135790 for this front-end demo — in production, Send code would call your backend to actually dispatch an OTP, and Verify would POST the entered digits to a real endpoint instead of comparing against a constant.