OTP Verification Screen — 2FA Code Input UI

OTP Verification Screen · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Auto-advancing digit boxes
Typing a digit moves focus forward and Backspace moves it back, so entering the code feels like typing one number.
Paste-to-fill
Pasting a 6-digit code into any box distributes it across all six and submits if complete.
Auto-submit on completion
verify() fires the moment the last digit lands, removing the separate "click Verify" step on the happy path.
One-time-code autofill
autocomplete="one-time-code" and inputmode="numeric" surface the SMS code and number pad on mobile.
Error shake and retry
A wrong code shakes the boxes, shows an error, then clears and refocuses the first box automatically.
Green success state
A correct code turns the boxes green, shows a verified status, and locks the inputs.
Resend cooldown timer
A live 30-second countdown gates the resend button, preventing request spam like a real backend.
Full keyboard navigation
Arrow keys, Backspace, and digit entry all move focus correctly across the six boxes.

About this UI Snippet

OTP Verification Screen — Auto-Advancing Code Boxes with Resend Timer

Screenshot of the OTP Verification Screen snippet rendered live

Entering a one-time passcode is a high-friction moment in any sign-in or sign-up flow — the user is switching between their inbox or SMS and your form, and every fumbled keystroke risks abandonment. A well-built OTP screen removes that friction: digits auto-advance, a pasted code fills every box at once, and the form submits itself the moment all six are entered. This snippet builds that complete verification screen in plain HTML, CSS, and vanilla JavaScript.

Auto-advance and backspace navigation

Each of the six boxes holds a single digit. Typing a number moves focus to the next box automatically, and pressing Backspace in an empty box jumps to the previous one — so the user never has to manually tab between fields. Arrow keys move left and right too. Non-numeric input is stripped on the fly (inputmode="numeric" also brings up the number pad on mobile), so the field only ever contains digits. This auto-advance behavior is what makes entering a six-digit code feel like typing one number rather than filling six separate inputs.

Paste the whole code at once

The single most important convenience: pasting "123456" (or a code copied from an email) into any box fills all six. The paste handler extracts the digits, distributes them across the inputs, focuses the next empty box, and — if the paste completed the code — submits immediately. Combined with the autocomplete="one-time-code" attribute (which lets iOS and Android offer the SMS code above the keyboard), this means a user can often verify in a single tap.

Auto-submit when complete

The moment the sixth digit lands — whether typed or pasted — verify() runs automatically, so there's no separate "now click Verify" step in the common case. The explicit Verify button remains for accessibility and as a fallback, but the happy path is hands-off. This is the behavior users expect from modern auth flows and it measurably reduces drop-off at the verification step.

Clear success and error feedback

A correct code turns the boxes green with a "Verified!" status and locks the inputs. An incorrect code shakes the boxes (a short translateX keyframe), shows an error message, then clears and refocuses the first box so the user can retry without manually deleting six digits. The shake is a well-established pattern for "wrong input, try again" that reads instantly without needing to read the text.

Resend with a cooldown

A "Resend code" button sits below, gated by a live 30-second countdown ("Resend in 27s") so a user can't spam resend requests — the same anti-abuse pattern a real auth backend enforces server-side. Resending clears the boxes, resets the state, and restarts the timer. This handles the genuinely common case where the first code never arrives, without letting it become a way to hammer your SMS/email provider.

Production notes

The demo checks against a hardcoded code; a real build replaces that with a fetch to your verification endpoint and shows the success or error state from its response. The verification must always happen server-side — a client-side comparison is purely for the demo. The component exposes the entered code and the verify hook cleanly, so wiring it to Auth0, Firebase, Supabase, or your own backend is a one-function change.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA verification screen renders with six code boxes; the first is focused and a resend countdown starts.
  2. 2
    Type the codeEnter digits — focus auto-advances, and when all six are filled the form verifies automatically (try 123456).
  3. 3
    Or paste itPaste a 6-digit code into any box — all six fill at once and it auto-submits.
  4. 4
    See error handlingEnter a wrong code — the boxes shake red, show an error, then clear and refocus for a retry.
  5. 5
    Resend after the cooldownWait out the 30-second timer, then click "Resend code" to reset and get a new code.
  6. 6
    Connect your auth backendReplace the hardcoded CORRECT check in verify() with a fetch to your server's verification endpoint.

Real-world uses

Common Use Cases

Two-factor authentication
The code-entry step after password sign-in — pair with a password requirements checklist on the signup side.
Email and phone verification
Confirm a new account's email or phone with a sent code during onboarding.
Passwordless login
Enter an emailed login code, complementing a magic link login as the code-based alternative.
Transaction and payment confirmation
Verify a sensitive action (transfer, purchase) with a one-time code.
Device and session authorization
Approve a new device sign-in with a code shown on a trusted device.
Learning multi-input UX
A reference for auto-advance, paste distribution, and auto-submit — compare with an OTP input for the bare input and a pin pad for keypad entry.

Got questions?

Frequently Asked Questions

Replace the hardcoded CORRECT comparison in verify() with a fetch POST to your verification endpoint, sending the entered code; show the success state on a 200 and the error state on a rejection. Verification must happen server-side — a client-side check is only for the demo, since anyone can read the expected code in the page source.

The paste handler intercepts the clipboard text, strips non-digits, takes the first six, and assigns one to each input in order, then focuses the next empty box. If the pasted value completes the code it auto-submits. This works no matter which box receives the paste, so the user can paste anywhere.

The autocomplete="one-time-code" attribute on the first input lets iOS and Android suggest the most recent SMS code above the keyboard for one-tap entry, and inputmode="numeric" brings up the number pad. For the autofill to match, the SMS should follow the platform's format (including your domain on iOS), which is configured in how you send the message, not the markup.

Once all six digits are entered there's no ambiguity about intent, so submitting automatically removes a redundant step and measurably cuts drop-off at the verification stage. The explicit Verify button stays as an accessible fallback and for users who paste a partial code, but the common path is hands-off.

In React, hold an array of six digit values in useState and refs for focus management, advancing focus in the onChange handler and verifying when full; in Vue, use a reactive array with template refs; in Angular, use a FormArray and ViewChildren. The auto-advance, paste, and cooldown logic port directly — only focus management uses each framework's ref mechanism.

OTP Verification Screen — Export as HTML, React, Vue, Angular & Tailwind

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>OTP Verification Screen</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:flex-start;justify-content:center;padding:50px 24px}
    
    .otv-card{background:#fff;border-radius:18px;padding:30px 28px;width:100%;max-width:380px;box-shadow:0 18px 44px rgba(15,23,42,.12);text-align:center}
    .otv-icon{width:52px;height:52px;border-radius:14px;background:linear-gradient(135deg,#6366f1,#8b5cf6);display:flex;align-items:center;justify-content:center;margin:0 auto 16px}
    .otv-card h2{font-size:19px;font-weight:800;color:#0f172a;margin-bottom:8px}
    .otv-card p{font-size:13px;color:#64748b;line-height:1.55;margin-bottom:20px}
    .otv-card p b{color:#1e293b}
    
    .otv-inputs{display:flex;gap:9px;justify-content:center;margin-bottom:14px}
    .otv-inputs input{width:46px;height:54px;border:1.5px solid #e2e8f0;border-radius:11px;text-align:center;font-size:22px;font-weight:800;color:#0f172a;font-family:inherit;transition:border-color .15s,box-shadow .15s}
    .otv-inputs input:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}
    .otv-inputs input.filled{border-color:#6366f1;background:#f5f3ff}
    .otv-inputs.error input{border-color:#ef4444;animation:otvShake .3s}
    .otv-inputs.success input{border-color:#22c55e;background:#f0fdf4}
    @keyframes otvShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}
    
    .otv-status{font-size:12.5px;font-weight:600;min-height:18px;margin-bottom:16px}
    .otv-status.err{color:#dc2626}
    .otv-status.ok{color:#16a34a}
    
    .otv-verify{width:100%;background:#6366f1;color:#fff;border:none;border-radius:10px;padding:12px;font-size:14px;font-weight:700;cursor:pointer;transition:background .15s,opacity .15s}
    .otv-verify:hover:not(:disabled){background:#4f46e5}
    .otv-verify:disabled{opacity:.45;cursor:not-allowed}
    
    .otv-resend{font-size:12.5px;color:#94a3b8;margin-top:16px}
    .otv-resend button{background:none;border:none;color:#6366f1;font-weight:700;cursor:pointer;font-size:12.5px}
    .otv-resend button:disabled{color:#94a3b8;cursor:default}
  </style>
</head>
<body>
  <div class="otv-card">
    <div class="otv-icon">
      <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
    </div>
    <h2>Verify your email</h2>
    <p>We sent a 6-digit code to <b>[email protected]</b>. Enter it below.</p>
  
    <div class="otv-inputs" id="otvInputs">
      <input type="text" inputmode="numeric" maxlength="1" autocomplete="one-time-code">
      <input type="text" inputmode="numeric" maxlength="1">
      <input type="text" inputmode="numeric" maxlength="1">
      <input type="text" inputmode="numeric" maxlength="1">
      <input type="text" inputmode="numeric" maxlength="1">
      <input type="text" inputmode="numeric" maxlength="1">
    </div>
  
    <p class="otv-status" id="otvStatus"></p>
  
    <button type="button" class="otv-verify" id="otvVerify" disabled>Verify</button>
    <p class="otv-resend">Didn't get it? <button type="button" id="otvResend" disabled>Resend code</button></p>
  </div>
  <script>
    var CORRECT = '123456';   // demo code — replace with a real server check
    var wrap = document.getElementById('otvInputs');
    var inputs = Array.prototype.slice.call(wrap.querySelectorAll('input'));
    var verifyBtn = document.getElementById('otvVerify');
    var statusEl = document.getElementById('otvStatus');
    var resendBtn = document.getElementById('otvResend');
    var cooldown = 0, timer = null;
    
    function code() { return inputs.map(function (i) { return i.value; }).join(''); }
    
    function refresh() {
      inputs.forEach(function (i) { i.classList.toggle('filled', i.value !== ''); });
      verifyBtn.disabled = code().length !== 6;
    }
    
    inputs.forEach(function (input, idx) {
      input.addEventListener('input', function () {
        this.value = this.value.replace(/[^0-9]/g, '').slice(0, 1);
        wrap.classList.remove('error');
        statusEl.textContent = '';
        if (this.value && idx < inputs.length - 1) inputs[idx + 1].focus();
        refresh();
        if (code().length === 6) verify();   // auto-submit when full
      });
      input.addEventListener('keydown', function (e) {
        if (e.key === 'Backspace' && !this.value && idx > 0) inputs[idx - 1].focus();
        if (e.key === 'ArrowLeft' && idx > 0) inputs[idx - 1].focus();
        if (e.key === 'ArrowRight' && idx < inputs.length - 1) inputs[idx + 1].focus();
      });
      input.addEventListener('paste', function (e) {
        e.preventDefault();
        var digits = (e.clipboardData || window.clipboardData).getData('text').replace(/[^0-9]/g, '').slice(0, 6);
        inputs.forEach(function (inp, i) { inp.value = digits[i] || ''; });
        refresh();
        (inputs[digits.length] || inputs[5]).focus();
        if (digits.length === 6) verify();
      });
    });
    
    function verify() {
      var entered = code();
      if (entered.length !== 6) return;
      // Replace with a real fetch to your verify endpoint.
      if (entered === CORRECT) {
        wrap.classList.remove('error'); wrap.classList.add('success');
        statusEl.textContent = '✓ Verified! Redirecting…'; statusEl.className = 'otv-status ok';
        verifyBtn.disabled = true; inputs.forEach(function (i) { i.disabled = true; });
      } else {
        wrap.classList.add('error');
        statusEl.textContent = 'That code is incorrect. Try again.'; statusEl.className = 'otv-status err';
        setTimeout(function () { inputs.forEach(function (i) { i.value = ''; }); inputs[0].focus(); refresh(); wrap.classList.remove('error'); }, 700);
      }
    }
    
    verifyBtn.addEventListener('click', verify);
    
    function startCooldown() {
      cooldown = 30; resendBtn.disabled = true;
      clearInterval(timer);
      timer = setInterval(function () {
        cooldown--;
        resendBtn.textContent = cooldown > 0 ? 'Resend in ' + cooldown + 's' : 'Resend code';
        if (cooldown <= 0) { clearInterval(timer); resendBtn.disabled = false; }
      }, 1000);
    }
    resendBtn.addEventListener('click', function () {
      if (this.disabled) return;
      inputs.forEach(function (i) { i.value = ''; i.disabled = false; }); refresh();
      wrap.className = 'otv-inputs'; statusEl.textContent = 'A new code is on its way.'; statusEl.className = 'otv-status ok';
      inputs[0].focus();
      startCooldown();
    });
    
    inputs[0].focus();
    startCooldown();
  </script>
</body>
</html>
Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>OTP Verification Screen</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    @keyframes otvShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}

    * {
      box-sizing:border-box;margin:0;padding:0
    }

    body {
      font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:flex-start;justify-content:center;padding:50px 24px
    }

    .otv-card h2 {
      font-size:19px;font-weight:800;color:#0f172a;margin-bottom:8px
    }

    .otv-card p {
      font-size:13px;color:#64748b;line-height:1.55;margin-bottom:20px
    }

    .otv-card p b {
      color:#1e293b
    }

    .otv-inputs {
      display:flex;gap:9px;justify-content:center;margin-bottom:14px
    }

    .otv-inputs input {
      width:46px;height:54px;border:1.5px solid #e2e8f0;border-radius:11px;text-align:center;font-size:22px;font-weight:800;color:#0f172a;font-family:inherit;transition:border-color .15s,box-shadow .15s
    }

    .otv-inputs input:focus {
      outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)
    }

    .otv-inputs input.filled {
      border-color:#6366f1;background:#f5f3ff
    }

    .otv-inputs.error input {
      border-color:#ef4444;animation:otvShake .3s
    }

    .otv-inputs.success input {
      border-color:#22c55e;background:#f0fdf4
    }

    .otv-status {
      font-size:12.5px;font-weight:600;min-height:18px;margin-bottom:16px
    }

    .otv-status.err {
      color:#dc2626
    }

    .otv-status.ok {
      color:#16a34a
    }

    .otv-verify:hover:not(:disabled) {
      background:#4f46e5
    }

    .otv-verify:disabled {
      opacity:.45;cursor:not-allowed
    }

    .otv-resend button {
      background:none;border:none;color:#6366f1;font-weight:700;cursor:pointer;font-size:12.5px
    }

    .otv-resend button:disabled {
      color:#94a3b8;cursor:default
    }
  </style>
</head>
<body>
  <div class="otv-card bg-[#fff] rounded-[18px] py-[30px] px-7 w-full max-w-[380px] shadow-[0_18px_44px_rgba(15,23,42,.12)] text-center">
    <div class="w-[52px] h-[52px] rounded-[14px] [background:linear-gradient(135deg,#6366f1,#8b5cf6)] flex items-center justify-center mt-0 mx-auto mb-4">
      <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
    </div>
    <h2>Verify your email</h2>
    <p>We sent a 6-digit code to <b>[email protected]</b>. Enter it below.</p>
  
    <div class="otv-inputs" id="otvInputs">
      <input type="text" inputmode="numeric" maxlength="1" autocomplete="one-time-code">
      <input type="text" inputmode="numeric" maxlength="1">
      <input type="text" inputmode="numeric" maxlength="1">
      <input type="text" inputmode="numeric" maxlength="1">
      <input type="text" inputmode="numeric" maxlength="1">
      <input type="text" inputmode="numeric" maxlength="1">
    </div>
  
    <p class="otv-status" id="otvStatus"></p>
  
    <button type="button" class="otv-verify w-full bg-[#6366f1] text-[#fff] border-0 rounded-[10px] p-3 text-sm font-bold cursor-pointer [transition:background_.15s,opacity_.15s]" id="otvVerify" disabled>Verify</button>
    <p class="otv-resend text-[12.5px] text-[#94a3b8] mt-4">Didn't get it? <button type="button" id="otvResend" disabled>Resend code</button></p>
  </div>
  <script>
    var CORRECT = '123456';   // demo code — replace with a real server check
    var wrap = document.getElementById('otvInputs');
    var inputs = Array.prototype.slice.call(wrap.querySelectorAll('input'));
    var verifyBtn = document.getElementById('otvVerify');
    var statusEl = document.getElementById('otvStatus');
    var resendBtn = document.getElementById('otvResend');
    var cooldown = 0, timer = null;
    
    function code() { return inputs.map(function (i) { return i.value; }).join(''); }
    
    function refresh() {
      inputs.forEach(function (i) { i.classList.toggle('filled', i.value !== ''); });
      verifyBtn.disabled = code().length !== 6;
    }
    
    inputs.forEach(function (input, idx) {
      input.addEventListener('input', function () {
        this.value = this.value.replace(/[^0-9]/g, '').slice(0, 1);
        wrap.classList.remove('error');
        statusEl.textContent = '';
        if (this.value && idx < inputs.length - 1) inputs[idx + 1].focus();
        refresh();
        if (code().length === 6) verify();   // auto-submit when full
      });
      input.addEventListener('keydown', function (e) {
        if (e.key === 'Backspace' && !this.value && idx > 0) inputs[idx - 1].focus();
        if (e.key === 'ArrowLeft' && idx > 0) inputs[idx - 1].focus();
        if (e.key === 'ArrowRight' && idx < inputs.length - 1) inputs[idx + 1].focus();
      });
      input.addEventListener('paste', function (e) {
        e.preventDefault();
        var digits = (e.clipboardData || window.clipboardData).getData('text').replace(/[^0-9]/g, '').slice(0, 6);
        inputs.forEach(function (inp, i) { inp.value = digits[i] || ''; });
        refresh();
        (inputs[digits.length] || inputs[5]).focus();
        if (digits.length === 6) verify();
      });
    });
    
    function verify() {
      var entered = code();
      if (entered.length !== 6) return;
      // Replace with a real fetch to your verify endpoint.
      if (entered === CORRECT) {
        wrap.classList.remove('error'); wrap.classList.add('success');
        statusEl.textContent = '✓ Verified! Redirecting…'; statusEl.className = 'otv-status ok';
        verifyBtn.disabled = true; inputs.forEach(function (i) { i.disabled = true; });
      } else {
        wrap.classList.add('error');
        statusEl.textContent = 'That code is incorrect. Try again.'; statusEl.className = 'otv-status err';
        setTimeout(function () { inputs.forEach(function (i) { i.value = ''; }); inputs[0].focus(); refresh(); wrap.classList.remove('error'); }, 700);
      }
    }
    
    verifyBtn.addEventListener('click', verify);
    
    function startCooldown() {
      cooldown = 30; resendBtn.disabled = true;
      clearInterval(timer);
      timer = setInterval(function () {
        cooldown--;
        resendBtn.textContent = cooldown > 0 ? 'Resend in ' + cooldown + 's' : 'Resend code';
        if (cooldown <= 0) { clearInterval(timer); resendBtn.disabled = false; }
      }, 1000);
    }
    resendBtn.addEventListener('click', function () {
      if (this.disabled) return;
      inputs.forEach(function (i) { i.value = ''; i.disabled = false; }); refresh();
      wrap.className = 'otv-inputs'; statusEl.textContent = 'A new code is on its way.'; statusEl.className = 'otv-status ok';
      inputs[0].focus();
      startCooldown();
    });
    
    inputs[0].focus();
    startCooldown();
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

// CSS — optionally move to OTPVerificationScreen.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:flex-start;justify-content:center;padding:50px 24px}

.otv-card{background:#fff;border-radius:18px;padding:30px 28px;width:100%;max-width:380px;box-shadow:0 18px 44px rgba(15,23,42,.12);text-align:center}
.otv-icon{width:52px;height:52px;border-radius:14px;background:linear-gradient(135deg,#6366f1,#8b5cf6);display:flex;align-items:center;justify-content:center;margin:0 auto 16px}
.otv-card h2{font-size:19px;font-weight:800;color:#0f172a;margin-bottom:8px}
.otv-card p{font-size:13px;color:#64748b;line-height:1.55;margin-bottom:20px}
.otv-card p b{color:#1e293b}

.otv-inputs{display:flex;gap:9px;justify-content:center;margin-bottom:14px}
.otv-inputs input{width:46px;height:54px;border:1.5px solid #e2e8f0;border-radius:11px;text-align:center;font-size:22px;font-weight:800;color:#0f172a;font-family:inherit;transition:border-color .15s,box-shadow .15s}
.otv-inputs input:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}
.otv-inputs input.filled{border-color:#6366f1;background:#f5f3ff}
.otv-inputs.error input{border-color:#ef4444;animation:otvShake .3s}
.otv-inputs.success input{border-color:#22c55e;background:#f0fdf4}
@keyframes otvShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}

.otv-status{font-size:12.5px;font-weight:600;min-height:18px;margin-bottom:16px}
.otv-status.err{color:#dc2626}
.otv-status.ok{color:#16a34a}

.otv-verify{width:100%;background:#6366f1;color:#fff;border:none;border-radius:10px;padding:12px;font-size:14px;font-weight:700;cursor:pointer;transition:background .15s,opacity .15s}
.otv-verify:hover:not(:disabled){background:#4f46e5}
.otv-verify:disabled{opacity:.45;cursor:not-allowed}

.otv-resend{font-size:12.5px;color:#94a3b8;margin-top:16px}
.otv-resend button{background:none;border:none;color:#6366f1;font-weight:700;cursor:pointer;font-size:12.5px}
.otv-resend button:disabled{color:#94a3b8;cursor:default}
`;

export default function OTPVerificationScreen() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var CORRECT = '123456';   // demo code — replace with a real server check
    var wrap = document.getElementById('otvInputs');
    var inputs = Array.prototype.slice.call(wrap.querySelectorAll('input'));
    var verifyBtn = document.getElementById('otvVerify');
    var statusEl = document.getElementById('otvStatus');
    var resendBtn = document.getElementById('otvResend');
    var cooldown = 0, timer = null;
    
    function code() { return inputs.map(function (i) { return i.value; }).join(''); }
    
    function refresh() {
      inputs.forEach(function (i) { i.classList.toggle('filled', i.value !== ''); });
      verifyBtn.disabled = code().length !== 6;
    }
    
    inputs.forEach(function (input, idx) {
      input.addEventListener('input', function () {
        this.value = this.value.replace(/[^0-9]/g, '').slice(0, 1);
        wrap.classList.remove('error');
        statusEl.textContent = '';
        if (this.value && idx < inputs.length - 1) inputs[idx + 1].focus();
        refresh();
        if (code().length === 6) verify();   // auto-submit when full
      });
      input.addEventListener('keydown', function (e) {
        if (e.key === 'Backspace' && !this.value && idx > 0) inputs[idx - 1].focus();
        if (e.key === 'ArrowLeft' && idx > 0) inputs[idx - 1].focus();
        if (e.key === 'ArrowRight' && idx < inputs.length - 1) inputs[idx + 1].focus();
      });
      input.addEventListener('paste', function (e) {
        e.preventDefault();
        var digits = (e.clipboardData || window.clipboardData).getData('text').replace(/[^0-9]/g, '').slice(0, 6);
        inputs.forEach(function (inp, i) { inp.value = digits[i] || ''; });
        refresh();
        (inputs[digits.length] || inputs[5]).focus();
        if (digits.length === 6) verify();
      });
    });
    
    function verify() {
      var entered = code();
      if (entered.length !== 6) return;
      // Replace with a real fetch to your verify endpoint.
      if (entered === CORRECT) {
        wrap.classList.remove('error'); wrap.classList.add('success');
        statusEl.textContent = '✓ Verified! Redirecting…'; statusEl.className = 'otv-status ok';
        verifyBtn.disabled = true; inputs.forEach(function (i) { i.disabled = true; });
      } else {
        wrap.classList.add('error');
        statusEl.textContent = 'That code is incorrect. Try again.'; statusEl.className = 'otv-status err';
        setTimeout(function () { inputs.forEach(function (i) { i.value = ''; }); inputs[0].focus(); refresh(); wrap.classList.remove('error'); }, 700);
      }
    }
    
    verifyBtn.addEventListener('click', verify);
    
    function startCooldown() {
      cooldown = 30; resendBtn.disabled = true;
      clearInterval(timer);
      timer = setInterval(function () {
        cooldown--;
        resendBtn.textContent = cooldown > 0 ? 'Resend in ' + cooldown + 's' : 'Resend code';
        if (cooldown <= 0) { clearInterval(timer); resendBtn.disabled = false; }
      }, 1000);
    }
    resendBtn.addEventListener('click', function () {
      if (this.disabled) return;
      inputs.forEach(function (i) { i.value = ''; i.disabled = false; }); refresh();
      wrap.className = 'otv-inputs'; statusEl.textContent = 'A new code is on its way.'; statusEl.className = 'otv-status ok';
      inputs[0].focus();
      startCooldown();
    });
    
    inputs[0].focus();
    startCooldown();
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{css}</style>
      <div className="otv-card">
        <div className="otv-icon">
          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
        </div>
        <h2>Verify your email</h2>
        <p>We sent a 6-digit code to <b>[email protected]</b>. Enter it below.</p>
      
        <div className="otv-inputs" id="otvInputs">
          <input type="text" inputmode="numeric" maxlength="1" autocomplete="one-time-code" />
          <input type="text" inputmode="numeric" maxlength="1" />
          <input type="text" inputmode="numeric" maxlength="1" />
          <input type="text" inputmode="numeric" maxlength="1" />
          <input type="text" inputmode="numeric" maxlength="1" />
          <input type="text" inputmode="numeric" maxlength="1" />
        </div>
      
        <p className="otv-status" id="otvStatus"></p>
      
        <button type="button" className="otv-verify" id="otvVerify" defaultDisabled>Verify</button>
        <p className="otv-resend">Didn't get it? <button type="button" id="otvResend" defaultDisabled>Resend code</button></p>
      </div>
    </>
  );
}
React + Tailwind
import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+

export default function OTPVerificationScreen() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var CORRECT = '123456';   // demo code — replace with a real server check
    var wrap = document.getElementById('otvInputs');
    var inputs = Array.prototype.slice.call(wrap.querySelectorAll('input'));
    var verifyBtn = document.getElementById('otvVerify');
    var statusEl = document.getElementById('otvStatus');
    var resendBtn = document.getElementById('otvResend');
    var cooldown = 0, timer = null;
    
    function code() { return inputs.map(function (i) { return i.value; }).join(''); }
    
    function refresh() {
      inputs.forEach(function (i) { i.classList.toggle('filled', i.value !== ''); });
      verifyBtn.disabled = code().length !== 6;
    }
    
    inputs.forEach(function (input, idx) {
      input.addEventListener('input', function () {
        this.value = this.value.replace(/[^0-9]/g, '').slice(0, 1);
        wrap.classList.remove('error');
        statusEl.textContent = '';
        if (this.value && idx < inputs.length - 1) inputs[idx + 1].focus();
        refresh();
        if (code().length === 6) verify();   // auto-submit when full
      });
      input.addEventListener('keydown', function (e) {
        if (e.key === 'Backspace' && !this.value && idx > 0) inputs[idx - 1].focus();
        if (e.key === 'ArrowLeft' && idx > 0) inputs[idx - 1].focus();
        if (e.key === 'ArrowRight' && idx < inputs.length - 1) inputs[idx + 1].focus();
      });
      input.addEventListener('paste', function (e) {
        e.preventDefault();
        var digits = (e.clipboardData || window.clipboardData).getData('text').replace(/[^0-9]/g, '').slice(0, 6);
        inputs.forEach(function (inp, i) { inp.value = digits[i] || ''; });
        refresh();
        (inputs[digits.length] || inputs[5]).focus();
        if (digits.length === 6) verify();
      });
    });
    
    function verify() {
      var entered = code();
      if (entered.length !== 6) return;
      // Replace with a real fetch to your verify endpoint.
      if (entered === CORRECT) {
        wrap.classList.remove('error'); wrap.classList.add('success');
        statusEl.textContent = '✓ Verified! Redirecting…'; statusEl.className = 'otv-status ok';
        verifyBtn.disabled = true; inputs.forEach(function (i) { i.disabled = true; });
      } else {
        wrap.classList.add('error');
        statusEl.textContent = 'That code is incorrect. Try again.'; statusEl.className = 'otv-status err';
        setTimeout(function () { inputs.forEach(function (i) { i.value = ''; }); inputs[0].focus(); refresh(); wrap.classList.remove('error'); }, 700);
      }
    }
    
    verifyBtn.addEventListener('click', verify);
    
    function startCooldown() {
      cooldown = 30; resendBtn.disabled = true;
      clearInterval(timer);
      timer = setInterval(function () {
        cooldown--;
        resendBtn.textContent = cooldown > 0 ? 'Resend in ' + cooldown + 's' : 'Resend code';
        if (cooldown <= 0) { clearInterval(timer); resendBtn.disabled = false; }
      }, 1000);
    }
    resendBtn.addEventListener('click', function () {
      if (this.disabled) return;
      inputs.forEach(function (i) { i.value = ''; i.disabled = false; }); refresh();
      wrap.className = 'otv-inputs'; statusEl.textContent = 'A new code is on its way.'; statusEl.className = 'otv-status ok';
      inputs[0].focus();
      startCooldown();
    });
    
    inputs[0].focus();
    startCooldown();
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{`
@keyframes otvShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}

* {
  box-sizing:border-box;margin:0;padding:0
}

body {
  font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:flex-start;justify-content:center;padding:50px 24px
}

.otv-card h2 {
  font-size:19px;font-weight:800;color:#0f172a;margin-bottom:8px
}

.otv-card p {
  font-size:13px;color:#64748b;line-height:1.55;margin-bottom:20px
}

.otv-card p b {
  color:#1e293b
}

.otv-inputs {
  display:flex;gap:9px;justify-content:center;margin-bottom:14px
}

.otv-inputs input {
  width:46px;height:54px;border:1.5px solid #e2e8f0;border-radius:11px;text-align:center;font-size:22px;font-weight:800;color:#0f172a;font-family:inherit;transition:border-color .15s,box-shadow .15s
}

.otv-inputs input:focus {
  outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)
}

.otv-inputs input.filled {
  border-color:#6366f1;background:#f5f3ff
}

.otv-inputs.error input {
  border-color:#ef4444;animation:otvShake .3s
}

.otv-inputs.success input {
  border-color:#22c55e;background:#f0fdf4
}

.otv-status {
  font-size:12.5px;font-weight:600;min-height:18px;margin-bottom:16px
}

.otv-status.err {
  color:#dc2626
}

.otv-status.ok {
  color:#16a34a
}

.otv-verify:hover:not(:disabled) {
  background:#4f46e5
}

.otv-verify:disabled {
  opacity:.45;cursor:not-allowed
}

.otv-resend button {
  background:none;border:none;color:#6366f1;font-weight:700;cursor:pointer;font-size:12.5px
}

.otv-resend button:disabled {
  color:#94a3b8;cursor:default
}
      `}</style>
      <div className="otv-card bg-[#fff] rounded-[18px] py-[30px] px-7 w-full max-w-[380px] shadow-[0_18px_44px_rgba(15,23,42,.12)] text-center">
        <div className="w-[52px] h-[52px] rounded-[14px] [background:linear-gradient(135deg,#6366f1,#8b5cf6)] flex items-center justify-center mt-0 mx-auto mb-4">
          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
        </div>
        <h2>Verify your email</h2>
        <p>We sent a 6-digit code to <b>[email protected]</b>. Enter it below.</p>
      
        <div className="otv-inputs" id="otvInputs">
          <input type="text" inputmode="numeric" maxlength="1" autocomplete="one-time-code" />
          <input type="text" inputmode="numeric" maxlength="1" />
          <input type="text" inputmode="numeric" maxlength="1" />
          <input type="text" inputmode="numeric" maxlength="1" />
          <input type="text" inputmode="numeric" maxlength="1" />
          <input type="text" inputmode="numeric" maxlength="1" />
        </div>
      
        <p className="otv-status" id="otvStatus"></p>
      
        <button type="button" className="otv-verify w-full bg-[#6366f1] text-[#fff] border-0 rounded-[10px] p-3 text-sm font-bold cursor-pointer [transition:background_.15s,opacity_.15s]" id="otvVerify" defaultDisabled>Verify</button>
        <p className="otv-resend text-[12.5px] text-[#94a3b8] mt-4">Didn't get it? <button type="button" id="otvResend" defaultDisabled>Resend code</button></p>
      </div>
    </>
  );
}
Vue
<template>
  <div class="otv-card">
    <div class="otv-icon">
      <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
    </div>
    <h2>Verify your email</h2>
    <p>We sent a 6-digit code to <b>[email protected]</b>. Enter it below.</p>
  
    <div class="otv-inputs" id="otvInputs">
      <input type="text" inputmode="numeric" maxlength="1" autocomplete="one-time-code">
      <input type="text" inputmode="numeric" maxlength="1">
      <input type="text" inputmode="numeric" maxlength="1">
      <input type="text" inputmode="numeric" maxlength="1">
      <input type="text" inputmode="numeric" maxlength="1">
      <input type="text" inputmode="numeric" maxlength="1">
    </div>
  
    <p class="otv-status" id="otvStatus"></p>
  
    <button type="button" class="otv-verify" id="otvVerify" disabled>Verify</button>
    <p class="otv-resend">Didn't get it? <button type="button" id="otvResend" disabled>Resend code</button></p>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';

var CORRECT = '123456';
let wrap;
let inputs;
let verifyBtn;
let statusEl;
let resendBtn;
var cooldown = 0, timer = null;
function code() { return inputs.map(function (i) { return i.value; }).join(''); }
function refresh() {
  inputs.forEach(function (i) { i.classList.toggle('filled', i.value !== ''); });
  verifyBtn.disabled = code().length !== 6;
}
function verify() {
  var entered = code();
  if (entered.length !== 6) return;
  // Replace with a real fetch to your verify endpoint.
  if (entered === CORRECT) {
    wrap.classList.remove('error'); wrap.classList.add('success');
    statusEl.textContent = '✓ Verified! Redirecting…'; statusEl.className = 'otv-status ok';
    verifyBtn.disabled = true; inputs.forEach(function (i) { i.disabled = true; });
  } else {
    wrap.classList.add('error');
    statusEl.textContent = 'That code is incorrect. Try again.'; statusEl.className = 'otv-status err';
    setTimeout(function () { inputs.forEach(function (i) { i.value = ''; }); inputs[0].focus(); refresh(); wrap.classList.remove('error'); }, 700);
  }
}
function startCooldown() {
  cooldown = 30; resendBtn.disabled = true;
  clearInterval(timer);
  timer = setInterval(function () {
    cooldown--;
    resendBtn.textContent = cooldown > 0 ? 'Resend in ' + cooldown + 's' : 'Resend code';
    if (cooldown <= 0) { clearInterval(timer); resendBtn.disabled = false; }
  }, 1000);
}

onMounted(() => {
  wrap = document.getElementById('otvInputs');
  inputs = Array.prototype.slice.call(wrap.querySelectorAll('input'));
  verifyBtn = document.getElementById('otvVerify');
  statusEl = document.getElementById('otvStatus');
  resendBtn = document.getElementById('otvResend');
  inputs.forEach(function (input, idx) {
    input.addEventListener('input', function () {
      this.value = this.value.replace(/[^0-9]/g, '').slice(0, 1);
      wrap.classList.remove('error');
      statusEl.textContent = '';
      if (this.value && idx < inputs.length - 1) inputs[idx + 1].focus();
      refresh();
      if (code().length === 6) verify();   // auto-submit when full
    });
    input.addEventListener('keydown', function (e) {
      if (e.key === 'Backspace' && !this.value && idx > 0) inputs[idx - 1].focus();
      if (e.key === 'ArrowLeft' && idx > 0) inputs[idx - 1].focus();
      if (e.key === 'ArrowRight' && idx < inputs.length - 1) inputs[idx + 1].focus();
    });
    input.addEventListener('paste', function (e) {
      e.preventDefault();
      var digits = (e.clipboardData || window.clipboardData).getData('text').replace(/[^0-9]/g, '').slice(0, 6);
      inputs.forEach(function (inp, i) { inp.value = digits[i] || ''; });
      refresh();
      (inputs[digits.length] || inputs[5]).focus();
      if (digits.length === 6) verify();
    });
  });
  verifyBtn.addEventListener('click', verify);
  resendBtn.addEventListener('click', function () {
    if (this.disabled) return;
    inputs.forEach(function (i) { i.value = ''; i.disabled = false; }); refresh();
    wrap.className = 'otv-inputs'; statusEl.textContent = 'A new code is on its way.'; statusEl.className = 'otv-status ok';
    inputs[0].focus();
    startCooldown();
  });
  inputs[0].focus();
  startCooldown();
});
</script>

<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:flex-start;justify-content:center;padding:50px 24px}

.otv-card{background:#fff;border-radius:18px;padding:30px 28px;width:100%;max-width:380px;box-shadow:0 18px 44px rgba(15,23,42,.12);text-align:center}
.otv-icon{width:52px;height:52px;border-radius:14px;background:linear-gradient(135deg,#6366f1,#8b5cf6);display:flex;align-items:center;justify-content:center;margin:0 auto 16px}
.otv-card h2{font-size:19px;font-weight:800;color:#0f172a;margin-bottom:8px}
.otv-card p{font-size:13px;color:#64748b;line-height:1.55;margin-bottom:20px}
.otv-card p b{color:#1e293b}

.otv-inputs{display:flex;gap:9px;justify-content:center;margin-bottom:14px}
.otv-inputs input{width:46px;height:54px;border:1.5px solid #e2e8f0;border-radius:11px;text-align:center;font-size:22px;font-weight:800;color:#0f172a;font-family:inherit;transition:border-color .15s,box-shadow .15s}
.otv-inputs input:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}
.otv-inputs input.filled{border-color:#6366f1;background:#f5f3ff}
.otv-inputs.error input{border-color:#ef4444;animation:otvShake .3s}
.otv-inputs.success input{border-color:#22c55e;background:#f0fdf4}
@keyframes otvShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}

.otv-status{font-size:12.5px;font-weight:600;min-height:18px;margin-bottom:16px}
.otv-status.err{color:#dc2626}
.otv-status.ok{color:#16a34a}

.otv-verify{width:100%;background:#6366f1;color:#fff;border:none;border-radius:10px;padding:12px;font-size:14px;font-weight:700;cursor:pointer;transition:background .15s,opacity .15s}
.otv-verify:hover:not(:disabled){background:#4f46e5}
.otv-verify:disabled{opacity:.45;cursor:not-allowed}

.otv-resend{font-size:12.5px;color:#94a3b8;margin-top:16px}
.otv-resend button{background:none;border:none;color:#6366f1;font-weight:700;cursor:pointer;font-size:12.5px}
.otv-resend button:disabled{color:#94a3b8;cursor:default}
</style>
Angular
// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-o-t-p-verification-screen',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="otv-card">
      <div class="otv-icon">
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
      </div>
      <h2>Verify your email</h2>
      <p>We sent a 6-digit code to <b>[email protected]</b>. Enter it below.</p>
    
      <div class="otv-inputs" id="otvInputs">
        <input type="text" inputmode="numeric" maxlength="1" autocomplete="one-time-code">
        <input type="text" inputmode="numeric" maxlength="1">
        <input type="text" inputmode="numeric" maxlength="1">
        <input type="text" inputmode="numeric" maxlength="1">
        <input type="text" inputmode="numeric" maxlength="1">
        <input type="text" inputmode="numeric" maxlength="1">
      </div>
    
      <p class="otv-status" id="otvStatus"></p>
    
      <button type="button" class="otv-verify" id="otvVerify" disabled>Verify</button>
      <p class="otv-resend">Didn't get it? <button type="button" id="otvResend" disabled>Resend code</button></p>
    </div>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:flex-start;justify-content:center;padding:50px 24px}
    
    .otv-card{background:#fff;border-radius:18px;padding:30px 28px;width:100%;max-width:380px;box-shadow:0 18px 44px rgba(15,23,42,.12);text-align:center}
    .otv-icon{width:52px;height:52px;border-radius:14px;background:linear-gradient(135deg,#6366f1,#8b5cf6);display:flex;align-items:center;justify-content:center;margin:0 auto 16px}
    .otv-card h2{font-size:19px;font-weight:800;color:#0f172a;margin-bottom:8px}
    .otv-card p{font-size:13px;color:#64748b;line-height:1.55;margin-bottom:20px}
    .otv-card p b{color:#1e293b}
    
    .otv-inputs{display:flex;gap:9px;justify-content:center;margin-bottom:14px}
    .otv-inputs input{width:46px;height:54px;border:1.5px solid #e2e8f0;border-radius:11px;text-align:center;font-size:22px;font-weight:800;color:#0f172a;font-family:inherit;transition:border-color .15s,box-shadow .15s}
    .otv-inputs input:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}
    .otv-inputs input.filled{border-color:#6366f1;background:#f5f3ff}
    .otv-inputs.error input{border-color:#ef4444;animation:otvShake .3s}
    .otv-inputs.success input{border-color:#22c55e;background:#f0fdf4}
    @keyframes otvShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}
    
    .otv-status{font-size:12.5px;font-weight:600;min-height:18px;margin-bottom:16px}
    .otv-status.err{color:#dc2626}
    .otv-status.ok{color:#16a34a}
    
    .otv-verify{width:100%;background:#6366f1;color:#fff;border:none;border-radius:10px;padding:12px;font-size:14px;font-weight:700;cursor:pointer;transition:background .15s,opacity .15s}
    .otv-verify:hover:not(:disabled){background:#4f46e5}
    .otv-verify:disabled{opacity:.45;cursor:not-allowed}
    
    .otv-resend{font-size:12.5px;color:#94a3b8;margin-top:16px}
    .otv-resend button{background:none;border:none;color:#6366f1;font-weight:700;cursor:pointer;font-size:12.5px}
    .otv-resend button:disabled{color:#94a3b8;cursor:default}
  `]
})
export class OTPVerificationScreenComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    var CORRECT = '123456';   // demo code — replace with a real server check
    var wrap = document.getElementById('otvInputs');
    var inputs = Array.prototype.slice.call(wrap.querySelectorAll('input'));
    var verifyBtn = document.getElementById('otvVerify');
    var statusEl = document.getElementById('otvStatus');
    var resendBtn = document.getElementById('otvResend');
    var cooldown = 0, timer = null;
    
    function code() { return inputs.map(function (i) { return i.value; }).join(''); }
    
    function refresh() {
      inputs.forEach(function (i) { i.classList.toggle('filled', i.value !== ''); });
      verifyBtn.disabled = code().length !== 6;
    }
    
    inputs.forEach(function (input, idx) {
      input.addEventListener('input', function () {
        this.value = this.value.replace(/[^0-9]/g, '').slice(0, 1);
        wrap.classList.remove('error');
        statusEl.textContent = '';
        if (this.value && idx < inputs.length - 1) inputs[idx + 1].focus();
        refresh();
        if (code().length === 6) verify();   // auto-submit when full
      });
      input.addEventListener('keydown', function (e) {
        if (e.key === 'Backspace' && !this.value && idx > 0) inputs[idx - 1].focus();
        if (e.key === 'ArrowLeft' && idx > 0) inputs[idx - 1].focus();
        if (e.key === 'ArrowRight' && idx < inputs.length - 1) inputs[idx + 1].focus();
      });
      input.addEventListener('paste', function (e) {
        e.preventDefault();
        var digits = (e.clipboardData || window.clipboardData).getData('text').replace(/[^0-9]/g, '').slice(0, 6);
        inputs.forEach(function (inp, i) { inp.value = digits[i] || ''; });
        refresh();
        (inputs[digits.length] || inputs[5]).focus();
        if (digits.length === 6) verify();
      });
    });
    
    function verify() {
      var entered = code();
      if (entered.length !== 6) return;
      // Replace with a real fetch to your verify endpoint.
      if (entered === CORRECT) {
        wrap.classList.remove('error'); wrap.classList.add('success');
        statusEl.textContent = '✓ Verified! Redirecting…'; statusEl.className = 'otv-status ok';
        verifyBtn.disabled = true; inputs.forEach(function (i) { i.disabled = true; });
      } else {
        wrap.classList.add('error');
        statusEl.textContent = 'That code is incorrect. Try again.'; statusEl.className = 'otv-status err';
        setTimeout(function () { inputs.forEach(function (i) { i.value = ''; }); inputs[0].focus(); refresh(); wrap.classList.remove('error'); }, 700);
      }
    }
    
    verifyBtn.addEventListener('click', verify);
    
    function startCooldown() {
      cooldown = 30; resendBtn.disabled = true;
      clearInterval(timer);
      timer = setInterval(function () {
        cooldown--;
        resendBtn.textContent = cooldown > 0 ? 'Resend in ' + cooldown + 's' : 'Resend code';
        if (cooldown <= 0) { clearInterval(timer); resendBtn.disabled = false; }
      }, 1000);
    }
    resendBtn.addEventListener('click', function () {
      if (this.disabled) return;
      inputs.forEach(function (i) { i.value = ''; i.disabled = false; }); refresh();
      wrap.className = 'otv-inputs'; statusEl.textContent = 'A new code is on its way.'; statusEl.className = 'otv-status ok';
      inputs[0].focus();
      startCooldown();
    });
    
    inputs[0].focus();
    startCooldown();

    // Bind inline-handler functions to the component so the template can call them
    Object.assign(this, { code, refresh, verify, startCooldown });
  }
}