Tailwind + JSMIT licensed

Tailwind OTP Code Input

Six single-character boxes in a <fieldset>, with auto-advance, backspace step-back and paste-to-fill. autocomplete=&quot;one-time-code&quot; on the first box is what surfaces the iOS SMS suggestion.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<div class="tif-10 grid min-h-svh w-full place-items-center bg-tif10-bg px-6 py-14 font-tif10-mono" aria-label="OTP code input demo">
  <fieldset class="grid w-full max-w-70 grid-cols-6 gap-2 border-0 p-0">
    <legend class="sr-only">Verification code</legend>
    <input type="text" inputmode="numeric" maxlength="1" autocomplete="one-time-code" pattern="[0-9]" aria-label="Digit 1" data-tif10
      class="aspect-square w-full rounded-[10px] border border-white/10 bg-tif10-field text-center text-lg font-bold text-tif10-ink caret-tif10-acc outline-none transition-[border-color,background-color,transform] duration-150 focus:-translate-y-px focus:border-tif10-acc focus:bg-tif10-acc/10 motion-reduce:transition-none motion-reduce:focus:translate-y-0">
    <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" aria-label="Digit 2" data-tif10
      class="aspect-square w-full rounded-[10px] border border-white/10 bg-tif10-field text-center text-lg font-bold text-tif10-ink caret-tif10-acc outline-none transition-[border-color,background-color,transform] duration-150 focus:-translate-y-px focus:border-tif10-acc focus:bg-tif10-acc/10 motion-reduce:transition-none motion-reduce:focus:translate-y-0">
    <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" aria-label="Digit 3" data-tif10
      class="aspect-square w-full rounded-[10px] border border-white/10 bg-tif10-field text-center text-lg font-bold text-tif10-ink caret-tif10-acc outline-none transition-[border-color,background-color,transform] duration-150 focus:-translate-y-px focus:border-tif10-acc focus:bg-tif10-acc/10 motion-reduce:transition-none motion-reduce:focus:translate-y-0">
    <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" aria-label="Digit 4" data-tif10
      class="aspect-square w-full rounded-[10px] border border-white/10 bg-tif10-field text-center text-lg font-bold text-tif10-ink caret-tif10-acc outline-none transition-[border-color,background-color,transform] duration-150 focus:-translate-y-px focus:border-tif10-acc focus:bg-tif10-acc/10 motion-reduce:transition-none motion-reduce:focus:translate-y-0">
    <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" aria-label="Digit 5" data-tif10
      class="aspect-square w-full rounded-[10px] border border-white/10 bg-tif10-field text-center text-lg font-bold text-tif10-ink caret-tif10-acc outline-none transition-[border-color,background-color,transform] duration-150 focus:-translate-y-px focus:border-tif10-acc focus:bg-tif10-acc/10 motion-reduce:transition-none motion-reduce:focus:translate-y-0">
    <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" aria-label="Digit 6" data-tif10
      class="aspect-square w-full rounded-[10px] border border-white/10 bg-tif10-field text-center text-lg font-bold text-tif10-ink caret-tif10-acc outline-none transition-[border-color,background-color,transform] duration-150 focus:-translate-y-px focus:border-tif10-acc focus:bg-tif10-acc/10 motion-reduce:transition-none motion-reduce:focus:translate-y-0">
  </fieldset>
</div>
@import "tailwindcss";

@theme {
  --color-tif10-bg: #12121a;
  --color-tif10-bg: oklch(0.19 0.015 285);
  --color-tif10-field: #1a1a22;
  --color-tif10-field: oklch(0.23 0.015 285);
  --color-tif10-ink: #f0eeff;
  --color-tif10-ink: oklch(0.95 0.02 290);
  --color-tif10-acc: #7c6cff;
  --color-tif10-acc: oklch(0.62 0.2 288);
  --font-tif10-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tif-10 {
  width: 100%;
}

.tif-10 ::selection {
  background: var(--color-tif10-acc);
  color: #12121a;
}
(function () {
  document.querySelectorAll('.tif-10').forEach(function (root) {
    if (root.dataset.bound) return; root.dataset.bound = '1';
    var inputs = Array.prototype.slice.call(root.querySelectorAll('[data-tif10]'));
    inputs.forEach(function (input, i) {
      input.addEventListener('input', function () {
        input.value = (input.value || '').replace(/\D/g, '').slice(0, 1);
        if (input.value && inputs[i + 1]) inputs[i + 1].focus();
      });
      input.addEventListener('keydown', function (e) {
        if (e.key === 'Backspace' && !input.value && inputs[i - 1]) {
          inputs[i - 1].focus();
        }
        if (e.key === 'ArrowLeft' && inputs[i - 1]) inputs[i - 1].focus();
        if (e.key === 'ArrowRight' && inputs[i + 1]) inputs[i + 1].focus();
      });
      input.addEventListener('paste', function (e) {
        var data = (e.clipboardData || window.clipboardData).getData('text');
        var digits = (data || '').replace(/\D/g, '').slice(0, inputs.length - i);
        if (!digits) return;
        e.preventDefault();
        for (var j = 0; j < digits.length; j++) {
          if (inputs[i + j]) inputs[i + j].value = digits[j];
        }
        inputs[Math.min(i + digits.length, inputs.length - 1)].focus();
      });
    });
  });
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tif10-bg: #12121a;
  --color-tif10-bg: oklch(0.19 0.015 285);
  --color-tif10-field: #1a1a22;
  --color-tif10-field: oklch(0.23 0.015 285);
  --color-tif10-ink: #f0eeff;
  --color-tif10-ink: oklch(0.95 0.02 290);
  --color-tif10-acc: #7c6cff;
  --color-tif10-acc: oklch(0.62 0.2 288);
  --font-tif10-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tif-10 {
  width: 100%;
}

.tif-10 ::selection {
  background: var(--color-tif10-acc);
  color: #12121a;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • Bug fixed. The original's regex is double-escaped inside its template literal and reaches the page as /\\D/g, which matches a literal backslash — so non-digits were never stripped. Single escape here.
  • Arrow-key navigation added: a six-box grid that only moves forward is a usability trap when you mistype box two.
  • The seven-property visually-hidden legend becomes sr-only.
  • The script is wrapped in an IIFE with a data-bound guard, so pasting the snippet twice on a page is safe.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind OTP Code Input
Source: https://codefronts.com/components/tailwind-css-input-fields/tailwind-otp-code-input/

Six single-character boxes in a <fieldset>, with auto-advance, backspace step-back and paste-to-fill. autocomplete="one-time-code" on the first box is what surfaces the iOS SMS suggestion.
## HTML
```html
<div class="tif-10 grid min-h-svh w-full place-items-center bg-tif10-bg px-6 py-14 font-tif10-mono" aria-label="OTP code input demo">
  <fieldset class="grid w-full max-w-70 grid-cols-6 gap-2 border-0 p-0">
    <legend class="sr-only">Verification code</legend>
    <input type="text" inputmode="numeric" maxlength="1" autocomplete="one-time-code" pattern="[0-9]" aria-label="Digit 1" data-tif10
      class="aspect-square w-full rounded-[10px] border border-white/10 bg-tif10-field text-center text-lg font-bold text-tif10-ink caret-tif10-acc outline-none transition-[border-color,background-color,transform] duration-150 focus:-translate-y-px focus:border-tif10-acc focus:bg-tif10-acc/10 motion-reduce:transition-none motion-reduce:focus:translate-y-0">
    <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" aria-label="Digit 2" data-tif10
      class="aspect-square w-full rounded-[10px] border border-white/10 bg-tif10-field text-center text-lg font-bold text-tif10-ink caret-tif10-acc outline-none transition-[border-color,background-color,transform] duration-150 focus:-translate-y-px focus:border-tif10-acc focus:bg-tif10-acc/10 motion-reduce:transition-none motion-reduce:focus:translate-y-0">
    <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" aria-label="Digit 3" data-tif10
      class="aspect-square w-full rounded-[10px] border border-white/10 bg-tif10-field text-center text-lg font-bold text-tif10-ink caret-tif10-acc outline-none transition-[border-color,background-color,transform] duration-150 focus:-translate-y-px focus:border-tif10-acc focus:bg-tif10-acc/10 motion-reduce:transition-none motion-reduce:focus:translate-y-0">
    <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" aria-label="Digit 4" data-tif10
      class="aspect-square w-full rounded-[10px] border border-white/10 bg-tif10-field text-center text-lg font-bold text-tif10-ink caret-tif10-acc outline-none transition-[border-color,background-color,transform] duration-150 focus:-translate-y-px focus:border-tif10-acc focus:bg-tif10-acc/10 motion-reduce:transition-none motion-reduce:focus:translate-y-0">
    <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" aria-label="Digit 5" data-tif10
      class="aspect-square w-full rounded-[10px] border border-white/10 bg-tif10-field text-center text-lg font-bold text-tif10-ink caret-tif10-acc outline-none transition-[border-color,background-color,transform] duration-150 focus:-translate-y-px focus:border-tif10-acc focus:bg-tif10-acc/10 motion-reduce:transition-none motion-reduce:focus:translate-y-0">
    <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" aria-label="Digit 6" data-tif10
      class="aspect-square w-full rounded-[10px] border border-white/10 bg-tif10-field text-center text-lg font-bold text-tif10-ink caret-tif10-acc outline-none transition-[border-color,background-color,transform] duration-150 focus:-translate-y-px focus:border-tif10-acc focus:bg-tif10-acc/10 motion-reduce:transition-none motion-reduce:focus:translate-y-0">
  </fieldset>
</div>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tif10-bg: #12121a;
  --color-tif10-bg: oklch(0.19 0.015 285);
  --color-tif10-field: #1a1a22;
  --color-tif10-field: oklch(0.23 0.015 285);
  --color-tif10-ink: #f0eeff;
  --color-tif10-ink: oklch(0.95 0.02 290);
  --color-tif10-acc: #7c6cff;
  --color-tif10-acc: oklch(0.62 0.2 288);
  --font-tif10-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tif-10 {
  width: 100%;
}

.tif-10 ::selection {
  background: var(--color-tif10-acc);
  color: #12121a;
}
```

## JavaScript
```js
(function () {
  document.querySelectorAll('.tif-10').forEach(function (root) {
    if (root.dataset.bound) return; root.dataset.bound = '1';
    var inputs = Array.prototype.slice.call(root.querySelectorAll('[data-tif10]'));
    inputs.forEach(function (input, i) {
      input.addEventListener('input', function () {
        input.value = (input.value || '').replace(/\D/g, '').slice(0, 1);
        if (input.value && inputs[i + 1]) inputs[i + 1].focus();
      });
      input.addEventListener('keydown', function (e) {
        if (e.key === 'Backspace' && !input.value && inputs[i - 1]) {
          inputs[i - 1].focus();
        }
        if (e.key === 'ArrowLeft' && inputs[i - 1]) inputs[i - 1].focus();
        if (e.key === 'ArrowRight' && inputs[i + 1]) inputs[i + 1].focus();
      });
      input.addEventListener('paste', function (e) {
        var data = (e.clipboardData || window.clipboardData).getData('text');
        var digits = (data || '').replace(/\D/g, '').slice(0, inputs.length - i);
        if (!digits) return;
        e.preventDefault();
        for (var j = 0; j < digits.length; j++) {
          if (inputs[i + j]) inputs[i + j].value = digits[j];
        }
        inputs[Math.min(i + digits.length, inputs.length - 1)].focus();
      });
    });
  });
})();
```

How this works

Three listeners per box. input strips non-digits and advances, keydown catches Backspace on an empty box and steps back, paste runs the same strip across the clipboard so 123-456 and code: 123 456 both land correctly.

The autofill contract is exact. iOS Safari looks for autocomplete="one-time-code", spelled that way, on the first field; Chrome Android's WebOTP API honours the same hint. A typo here does not warn, it just silently never offers the code.

Make it yours

  • Digit count: add or remove inputs — the script reads whatever it finds and adapts grid-cols-6 is the only thing to match.
  • Alphanumeric: drop inputmode=&quot;numeric&quot; and the digits-only strip.
  • Auto-submit: in the input handler, check i === inputs.length - 1 && input.value and call form.requestSubmit().
  • Accent: --color-tif10-acc drives caret, focus border and focus fill.

Gotchas — read before shipping

  • iOS SMS autofill no-ops on any spelling other than one-time-code — no shorthand, no typo.
  • Paste only survives 123-456 because the regex strips non-digits first. Remove it and users see empty boxes.
  • Calling focus() mid-render inside React loses the caret — wire this through refs and an effect, not inline JSX.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

iOS Safari 12+ surfaces the SMS suggestion via autocomplete="one-time-code"; Chrome Android 84+ implements WebOTP for the same UX.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Search CodeFronts

Loading…