webdevpuneet.com

OTP Input — Free HTML CSS JS 6-Digit Code Snippet

OTP / PIN Input · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

Copy & paste

Source Code

<div class="demo">
  <h3>Verify your email</h3>
  <p>Enter the 6-digit code sent to [email protected]</p>
  <div class="otp" id="otp">
    <input maxlength="1" type="text" inputmode="numeric" />
    <input maxlength="1" type="text" inputmode="numeric" />
    <input maxlength="1" type="text" inputmode="numeric" />
    <span class="sep">—</span>
    <input maxlength="1" type="text" inputmode="numeric" />
    <input maxlength="1" type="text" inputmode="numeric" />
    <input maxlength="1" type="text" inputmode="numeric" />
  </div>
  <button class="btn" onclick="verify()">Verify</button>
  <p class="resend">Didn't receive it? <a href="#">Resend code</a></p>
</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

Auto-focus advance: inputs[i + 1].focus() after each digit
Non-digit rejection: inp.value.replace(/\D/g, "") strips letters and symbols
Paste-to-fill: distributes clipboard characters across inputs, moves focus to last
Backspace navigation: moves to previous input if current is empty
Filled box highlight: .filled class adds accent border and light background
Separator dot between digit groups (3+3) for readability
Monospace font for digit alignment in each box
Export as HTML file, React JSX, or React + Tailwind CSS
Mobile (375px), Tablet (768px), Desktop device preview buttons
Live split-pane editor — preview updates as you type

About this UI Snippet

OTP / PIN Input — Auto-Focus Advance, Paste Handler & Backspace Navigation

Screenshot of the OTP / PIN Input snippet rendered live

An OTP (one-time password) or PIN input is a row of individual character boxes used for verification codes, 2FA authentication, and payment PINs — see the full OTP verification flow and the keypad-style PIN pad, or skip codes entirely with a magic link. Each box accepts one character and the cursor automatically moves to the next box after input. This snippet implements the complete interaction: auto-focus advance, paste-to-fill, backspace-to-previous, and a filled box highlight.

Auto-focus advance

The input event listener strips non-digit characters with inp.value = inp.value.replace(/\D/g, ''), then checks if (inp.value && i < inputs.length - 1) inputs[i + 1].focus(). Replacing non-digits means letters and symbols are silently rejected — only numbers pass. Auto-advancing after the digit is entered means users never need to click the next box.

Paste-to-fill

The paste event handler prevents the default paste (which would place all text in one box), extracts the clipboard text, strips non-digits, slices to the number of inputs, and distributes each character to the corresponding input: paste.split('').forEach((ch, j) => { if (inputs[j]) { inputs[j].value = ch; inputs[j].classList.add('filled'); } }). Focus moves to the box after the last filled character.

Backspace navigation

The keydown listener checks if (e.key === 'Backspace' && !inp.value && i > 0) inputs[i - 1].focus(). If Backspace is pressed on an empty box, focus moves back to the previous box. If the box has a value, the default behaviour clears it without moving focus — the user can then press Backspace again to go back.

The filled highlight

.otp input.filled { border-color: #6366f1; background: #eef2ff; } gives filled boxes a visible indicator that they contain a value. The class is added on input and removed if the value is cleared.

Verification

The verify() function joins all input values and checks if the length equals 6. Replace the alert with your API verification call.

Auto-advance focus

On each input event, if the current input has a value (exactly one digit), focus() moves to the next input: inputs[i+1].focus(). This creates the auto-advance experience users expect from OTP fields. The input uses maxlength="1" to limit entry to one character. On input, the current value is sanitised to only digits: input.value = input.value.replace(/D/g, '').slice(-1) — the slice(-1) keeps only the last entered character in case the browser's input event fires with multiple characters.

Paste handling

A paste event listener on the first input (or any input) reads e.clipboardData.getData('text') and distributes the pasted characters across all inputs: pastedText.split('').forEach((char, i) => { if (inputs[i]) inputs[i].value = char; }). Focus moves to the last filled input or the first empty one after paste. This is the critical OTP UX feature — paste an SMS code and the entire field fills instantly.

Backspace navigation

A keydown listener on each input checks for Backspace. If the current input is already empty and Backspace is pressed, focus moves to the previous input: inputs[i-1].focus(). This allows the user to correct a mistake without clicking the previous box.

Step by step

How to Use

  1. 1
    Type in the first boxClick the first box in the preview and type digits. Focus automatically moves to the next box after each digit.
  2. 2
    Test pasteCopy "123456" and paste it into the first box. All six boxes fill instantly and focus moves to the last box.
  3. 3
    Test backspaceWith a filled box focused, press Backspace to clear it. Press again on an empty box to move focus back.
  4. 4
    Change the digit countIn the HTML panel, add or remove input elements inside #otp. The JS uses querySelectorAll so it picks up any number of inputs.
  5. 5
    Connect to verificationIn the JS panel, replace the alert in verify() with your API call: fetch("/verify", { method: "POST", body: JSON.stringify({ code }) }).
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

2FA and two-step verification
The standard OTP input for SMS and authenticator app codes. Auto-advance and paste support make it fast for users receiving codes on mobile.
Email verification on sign-up
Verify email ownership during registration with a 6-digit code. The large monospace boxes make it easy to type a code from a separate device.
Payment PIN and checkout security
Use a 4-digit variant for payment confirmation PINs. The backspace navigation matches the behaviour users expect from native PIN inputs.
Learn paste event handling
The paste handler prevents default, reads clipboard data, and distributes characters across elements. Edit the JS panel to understand how paste events work differently from input events.
Account recovery codes
Use for 8-character recovery code input during account unlock flows. Change the digit count in the HTML and the slice length in the JS.
Migrate to a React OTP component
Use the JSX export as a starting point. In React, manage each input value in a useState array and wire the advance/backspace logic as controlled input handlers.
Related: Transfer List
See the Transfer List for a related forms pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

The input event fires after each character is typed. After stripping non-digits and confirming the value is non-empty, inputs[i + 1].focus() moves focus to the next input. On the last input, no advance occurs.

The paste event calls e.preventDefault() to block default paste. It reads e.clipboardData.getData("text"), strips non-digits, and slices to the input count. It then loops through the characters and sets each inputs[j].value = character, filling boxes left to right.

The keydown handler checks e.key === "Backspace" and !inp.value (the current box is empty). If both are true, inputs[i - 1].focus() moves back. If the box has a value, the default Backspace clears it first — the user presses again to go back.

In the HTML panel, remove two input elements from the #otp div. The querySelectorAll selector picks up any number of inputs automatically. Also remove the .sep separator if you have one between the groups.

inputs.map(i => i.value).join("") concatenates all values into a string. The verify() function already does this. Replace the alert with your fetch() API call, passing the code as a request body parameter.

Yes. Click "JSX" to download a React component. In React, manage a values array in useState. Each input is controlled: value={values[i]}. Update the array on change: setValues(prev => { const next = [...prev]; next[i] = e.target.value.replace(/\D/g,""); return next; }). Use refs for focus management.