25 / 30
All 30 in 30 Tailwind CSS Login Forms
- 01Tailwind Split-Screen Brand Hero Login Form
- 02Tailwind Multi-Tenant Workspace Account Switcher
- 03Tailwind Floating Label Login Form with peer Variants
- 04Tailwind Web3 Wallet Connect Login Grid
- 05Tailwind Minimalist Fintech Login Card with Layered Shadow
- 06Tailwind OAuth Social Login Form with Brand SVG Buttons
- 07Tailwind Passwordless Magic Link Login with data-state Machine
- 08Tailwind Aurora Glow Login Card with Drifting Blur Blobs
- 09Tailwind Neon Synthwave Arcade Login Form
- 10Tailwind Sliding Login and Sign Up Panel with peer-checked
- 11Tailwind 3D Flip Login Card with Password Reset Back Face
- 12Tailwind Terminal CLI Prompt Login Form
- 13Tailwind Floating Pill Login Widget Docked Over App Content
- 14Tailwind Gradient Border Login Card with background-clip
- 15Tailwind Slide-Over Side Panel Login without JavaScript
- 16Tailwind Social-First Sign Up with Brand-Compliant Buttons
- 17Tailwind Animated Mascot Login that Covers Its Eyes
- 18Tailwind Accessible Compact Login Modal with Scale-In
- 19Tailwind Glassmorphism Login Card with backdrop-filter
- 20Tailwind Multi-Step Login Wizard with Radio-Driven Track
- 21Tailwind Animated Conic Gradient Border Login Card
- 22Tailwind Secure Vault Login with user-invalid Error State
- 23Tailwind Neo-Brutalist Login Form with Hard Offset Shadow
- 24Tailwind Show Hide Password Toggle with aria-pressed
- 25Tailwind OTP Code Input Grid with Auto-Advance
- 26Tailwind Password Strength Meter with data-score Variants
- 27Tailwind Numeric PIN Pad Login for Kiosks and POS
- 28Tailwind Inline Form Validation with user-valid and user-invalid
- 29Tailwind Particle Constellation Login over Canvas
- 30Tailwind Biometric Face ID Prompt with Pulse Ripples
Tailwind OTP Code Input Grid with Auto-Advance
The multi-factor confirmation screen: six evenly-sized digit boxes that auto-advance as you type, accept a full paste, and step back on Backspace. CSS holds the filled state via not-placeholder-shown:, so the boxes light up without the script tracking any visual state at all.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tlf-25 w-full min-h-svh grid place-items-center bg-tlf25-stage font-tlf25-sans p-6">
<form novalidate class="tlf-25-form w-[min(420px,100%)] p-8 text-center bg-white border border-tlf25-edge rounded-[18px] shadow-[0_22px_50px_-38px_oklch(0.25_0.06_265/0.5)]">
<h1 class="text-[22px] font-bold text-tlf25-ink">Verify your identity</h1>
<p class="mt-1.5 mb-6 text-[13.5px]/[1.5] text-tlf25-mut">Enter the 6-digit code we sent to your email.</p>
<div role="group" aria-label="6-digit verification code" class="grid grid-cols-6 gap-2 mb-6">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 1">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 2">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 3">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 4">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 5">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 6">
</div>
<button type="submit" class="w-full h-[50px] rounded-xl border-0 cursor-pointer bg-tlf25-acc text-white text-[15px] font-semibold transition-[filter,scale] duration-150 hover:brightness-110 active:scale-[0.99] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf25-ring motion-reduce:transition-none motion-reduce:active:scale-100">Verify</button>
<p class="mt-4 text-[13px] text-tlf25-mut">Didn’t get it? <a href="#" class="font-semibold text-tlf25-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tlf25-acc">Resend code</a></p>
</form>
</section><section class="tlf-25 w-full min-h-svh grid place-items-center bg-tlf25-stage font-tlf25-sans p-6">
<form novalidate class="tlf-25-form w-[min(420px,100%)] p-8 text-center bg-white border border-tlf25-edge rounded-[18px] shadow-[0_22px_50px_-38px_oklch(0.25_0.06_265/0.5)]">
<h1 class="text-[22px] font-bold text-tlf25-ink">Verify your identity</h1>
<p class="mt-1.5 mb-6 text-[13.5px]/[1.5] text-tlf25-mut">Enter the 6-digit code we sent to your email.</p>
<div role="group" aria-label="6-digit verification code" class="grid grid-cols-6 gap-2 mb-6">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 1">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 2">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 3">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 4">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 5">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 6">
</div>
<button type="submit" class="w-full h-[50px] rounded-xl border-0 cursor-pointer bg-tlf25-acc text-white text-[15px] font-semibold transition-[filter,scale] duration-150 hover:brightness-110 active:scale-[0.99] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf25-ring motion-reduce:transition-none motion-reduce:active:scale-100">Verify</button>
<p class="mt-4 text-[13px] text-tlf25-mut">Didn’t get it? <a href="#" class="font-semibold text-tlf25-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tlf25-acc">Resend code</a></p>
</form>
</section>@import "tailwindcss";
@theme {
--color-tlf25-stage: #eef1f7;
--color-tlf25-stage: oklch(0.95 0.008 265);
--color-tlf25-edge: #e5e9f2;
--color-tlf25-edge: oklch(0.92 0.008 265);
--color-tlf25-fieldedge: #d5dbe8;
--color-tlf25-fieldedge: oklch(0.87 0.012 265);
--color-tlf25-field: #fafbfe;
--color-tlf25-field: oklch(0.99 0.003 265);
--color-tlf25-tint: #eef1fc;
--color-tlf25-tint: oklch(0.56 0.18 265 / 0.06);
--color-tlf25-ink: #1a2237;
--color-tlf25-ink: oklch(0.25 0.04 265);
--color-tlf25-mut: #666e88;
--color-tlf25-mut: oklch(0.52 0.03 265);
--color-tlf25-ph: #bcc3d4;
--color-tlf25-ph: oklch(0.8 0.015 265);
--color-tlf25-acc: #4159cd;
--color-tlf25-acc: oklch(0.51 0.18 265);
--color-tlf25-halo: #bfc9f0;
--color-tlf25-halo: oklch(0.51 0.18 265 / 0.2);
--color-tlf25-ring: #94a2ea;
--color-tlf25-ring: oklch(0.51 0.18 265 / 0.5);
--font-tlf25-sans: 'DM Sans', system-ui, sans-serif;
}
.tlf-25 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tlf25-stage: #eef1f7;
--color-tlf25-stage: oklch(0.95 0.008 265);
--color-tlf25-edge: #e5e9f2;
--color-tlf25-edge: oklch(0.92 0.008 265);
--color-tlf25-fieldedge: #d5dbe8;
--color-tlf25-fieldedge: oklch(0.87 0.012 265);
--color-tlf25-field: #fafbfe;
--color-tlf25-field: oklch(0.99 0.003 265);
--color-tlf25-tint: #eef1fc;
--color-tlf25-tint: oklch(0.56 0.18 265 / 0.06);
--color-tlf25-ink: #1a2237;
--color-tlf25-ink: oklch(0.25 0.04 265);
--color-tlf25-mut: #666e88;
--color-tlf25-mut: oklch(0.52 0.03 265);
--color-tlf25-ph: #bcc3d4;
--color-tlf25-ph: oklch(0.8 0.015 265);
--color-tlf25-acc: #4159cd;
--color-tlf25-acc: oklch(0.51 0.18 265);
--color-tlf25-halo: #bfc9f0;
--color-tlf25-halo: oklch(0.51 0.18 265 / 0.2);
--color-tlf25-ring: #94a2ea;
--color-tlf25-ring: oklch(0.51 0.18 265 / 0.5);
--font-tlf25-sans: 'DM Sans', system-ui, sans-serif;
}
.tlf-25 {
width: 100%;
min-height: 100svh;
}const boxes = [...document.querySelectorAll('.tlf-25-box')];
boxes.forEach((box, i) => {
box.addEventListener('input', () => {
box.value = box.value.replace(/[^0-9]/g, '').slice(0, 1);
if (box.value && i < boxes.length - 1) boxes[i + 1].focus();
});
box.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !box.value && i > 0) boxes[i - 1].focus();
if (e.key === 'ArrowLeft' && i > 0) boxes[i - 1].focus();
if (e.key === 'ArrowRight' && i < boxes.length - 1) boxes[i + 1].focus();
});
box.addEventListener('paste', (e) => {
e.preventDefault();
const digits = (e.clipboardData.getData('text') || '').replace(/[^0-9]/g, '').split('');
boxes.forEach((b, j) => { if (digits[j] != null) b.value = digits[j]; });
boxes[Math.min(digits.length, boxes.length) - 1]?.focus();
});
});const boxes = [...document.querySelectorAll('.tlf-25-box')];
boxes.forEach((box, i) => {
box.addEventListener('input', () => {
box.value = box.value.replace(/[^0-9]/g, '').slice(0, 1);
if (box.value && i < boxes.length - 1) boxes[i + 1].focus();
});
box.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !box.value && i > 0) boxes[i - 1].focus();
if (e.key === 'ArrowLeft' && i > 0) boxes[i - 1].focus();
if (e.key === 'ArrowRight' && i < boxes.length - 1) boxes[i + 1].focus();
});
box.addEventListener('paste', (e) => {
e.preventDefault();
const digits = (e.clipboardData.getData('text') || '').replace(/[^0-9]/g, '').split('');
boxes.forEach((b, j) => { if (digits[j] != null) b.value = digits[j]; });
boxes[Math.min(digits.length, boxes.length) - 1]?.focus();
});
});Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tlf25-stage: #eef1f7;
--color-tlf25-stage: oklch(0.95 0.008 265);
--color-tlf25-edge: #e5e9f2;
--color-tlf25-edge: oklch(0.92 0.008 265);
--color-tlf25-fieldedge: #d5dbe8;
--color-tlf25-fieldedge: oklch(0.87 0.012 265);
--color-tlf25-field: #fafbfe;
--color-tlf25-field: oklch(0.99 0.003 265);
--color-tlf25-tint: #eef1fc;
--color-tlf25-tint: oklch(0.56 0.18 265 / 0.06);
--color-tlf25-ink: #1a2237;
--color-tlf25-ink: oklch(0.25 0.04 265);
--color-tlf25-mut: #666e88;
--color-tlf25-mut: oklch(0.52 0.03 265);
--color-tlf25-ph: #bcc3d4;
--color-tlf25-ph: oklch(0.8 0.015 265);
--color-tlf25-acc: #4159cd;
--color-tlf25-acc: oklch(0.51 0.18 265);
--color-tlf25-halo: #bfc9f0;
--color-tlf25-halo: oklch(0.51 0.18 265 / 0.2);
--color-tlf25-ring: #94a2ea;
--color-tlf25-ring: oklch(0.51 0.18 265 / 0.5);
--font-tlf25-sans: 'DM Sans', system-ui, sans-serif;
}
.tlf-25 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tlf25-stage: #eef1f7;
--color-tlf25-stage: oklch(0.95 0.008 265);
--color-tlf25-edge: #e5e9f2;
--color-tlf25-edge: oklch(0.92 0.008 265);
--color-tlf25-fieldedge: #d5dbe8;
--color-tlf25-fieldedge: oklch(0.87 0.012 265);
--color-tlf25-field: #fafbfe;
--color-tlf25-field: oklch(0.99 0.003 265);
--color-tlf25-tint: #eef1fc;
--color-tlf25-tint: oklch(0.56 0.18 265 / 0.06);
--color-tlf25-ink: #1a2237;
--color-tlf25-ink: oklch(0.25 0.04 265);
--color-tlf25-mut: #666e88;
--color-tlf25-mut: oklch(0.52 0.03 265);
--color-tlf25-ph: #bcc3d4;
--color-tlf25-ph: oklch(0.8 0.015 265);
--color-tlf25-acc: #4159cd;
--color-tlf25-acc: oklch(0.51 0.18 265);
--color-tlf25-halo: #bfc9f0;
--color-tlf25-halo: oklch(0.51 0.18 265 / 0.2);
--color-tlf25-ring: #94a2ea;
--color-tlf25-ring: oklch(0.51 0.18 265 / 0.5);
--font-tlf25-sans: 'DM Sans', system-ui, sans-serif;
}
.tlf-25 {
width: 100%;
min-height: 100svh;
}<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=DM+Sans:wght@400;500;600;700&display=swap">
<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=DM+Sans:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
not-placeholder-shown:is v4's positive variant for:not(:placeholder-shown), and it is exactly what an OTP grid wants: the filled look is a function of the value, so the script never touches presentation.- Six near-identical class lists is the honest cost of utilities on repeated elements. In a real codebase this is one component rendered six times — the collection keeps them expanded because the whole point is to show the class list.
- Arrow-key navigation was added to the handler: a six-box field where ←/→ do nothing feels broken, and the original only handled Backspace.
- UI change from the CSS original: DM Sans replaces the system stack, the muted sub-copy lifted from
#767d97tooklch(0.52 0.03 265), boxes went 52px →h-13with a 48px narrow variant, and the filled tint is now a token rather than an inline alpha colour.
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 Grid with Auto-Advance
Source: https://codefronts.com/components/tailwind-css-login-forms/tailwind-otp-code-input-grid-auto-advance/
The multi-factor confirmation screen: six evenly-sized digit boxes that auto-advance as you type, accept a full paste, and step back on Backspace. CSS holds the filled state via not-placeholder-shown:, so the boxes light up without the script tracking any visual state at all.
## HTML
```html
<section class="tlf-25 w-full min-h-svh grid place-items-center bg-tlf25-stage font-tlf25-sans p-6">
<form novalidate class="tlf-25-form w-[min(420px,100%)] p-8 text-center bg-white border border-tlf25-edge rounded-[18px] shadow-[0_22px_50px_-38px_oklch(0.25_0.06_265/0.5)]">
<h1 class="text-[22px] font-bold text-tlf25-ink">Verify your identity</h1>
<p class="mt-1.5 mb-6 text-[13.5px]/[1.5] text-tlf25-mut">Enter the 6-digit code we sent to your email.</p>
<div role="group" aria-label="6-digit verification code" class="grid grid-cols-6 gap-2 mb-6">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 1">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 2">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 3">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 4">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 5">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 6">
</div>
<button type="submit" class="w-full h-[50px] rounded-xl border-0 cursor-pointer bg-tlf25-acc text-white text-[15px] font-semibold transition-[filter,scale] duration-150 hover:brightness-110 active:scale-[0.99] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf25-ring motion-reduce:transition-none motion-reduce:active:scale-100">Verify</button>
<p class="mt-4 text-[13px] text-tlf25-mut">Didn’t get it? <a href="#" class="font-semibold text-tlf25-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tlf25-acc">Resend code</a></p>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tlf25-stage: #eef1f7;
--color-tlf25-stage: oklch(0.95 0.008 265);
--color-tlf25-edge: #e5e9f2;
--color-tlf25-edge: oklch(0.92 0.008 265);
--color-tlf25-fieldedge: #d5dbe8;
--color-tlf25-fieldedge: oklch(0.87 0.012 265);
--color-tlf25-field: #fafbfe;
--color-tlf25-field: oklch(0.99 0.003 265);
--color-tlf25-tint: #eef1fc;
--color-tlf25-tint: oklch(0.56 0.18 265 / 0.06);
--color-tlf25-ink: #1a2237;
--color-tlf25-ink: oklch(0.25 0.04 265);
--color-tlf25-mut: #666e88;
--color-tlf25-mut: oklch(0.52 0.03 265);
--color-tlf25-ph: #bcc3d4;
--color-tlf25-ph: oklch(0.8 0.015 265);
--color-tlf25-acc: #4159cd;
--color-tlf25-acc: oklch(0.51 0.18 265);
--color-tlf25-halo: #bfc9f0;
--color-tlf25-halo: oklch(0.51 0.18 265 / 0.2);
--color-tlf25-ring: #94a2ea;
--color-tlf25-ring: oklch(0.51 0.18 265 / 0.5);
--font-tlf25-sans: 'DM Sans', system-ui, sans-serif;
}
.tlf-25 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
const boxes = [...document.querySelectorAll('.tlf-25-box')];
boxes.forEach((box, i) => {
box.addEventListener('input', () => {
box.value = box.value.replace(/[^0-9]/g, '').slice(0, 1);
if (box.value && i < boxes.length - 1) boxes[i + 1].focus();
});
box.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !box.value && i > 0) boxes[i - 1].focus();
if (e.key === 'ArrowLeft' && i > 0) boxes[i - 1].focus();
if (e.key === 'ArrowRight' && i < boxes.length - 1) boxes[i + 1].focus();
});
box.addEventListener('paste', (e) => {
e.preventDefault();
const digits = (e.clipboardData.getData('text') || '').replace(/[^0-9]/g, '').split('');
boxes.forEach((b, j) => { if (digits[j] != null) b.value = digits[j]; });
boxes[Math.min(digits.length, boxes.length) - 1]?.focus();
});
});
```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 Grid with Auto-Advance
Source: https://codefronts.com/components/tailwind-css-login-forms/tailwind-otp-code-input-grid-auto-advance/
The multi-factor confirmation screen: six evenly-sized digit boxes that auto-advance as you type, accept a full paste, and step back on Backspace. CSS holds the filled state via not-placeholder-shown:, so the boxes light up without the script tracking any visual state at all.
## HTML
```html
<section class="tlf-25 w-full min-h-svh grid place-items-center bg-tlf25-stage font-tlf25-sans p-6">
<form novalidate class="tlf-25-form w-[min(420px,100%)] p-8 text-center bg-white border border-tlf25-edge rounded-[18px] shadow-[0_22px_50px_-38px_oklch(0.25_0.06_265/0.5)]">
<h1 class="text-[22px] font-bold text-tlf25-ink">Verify your identity</h1>
<p class="mt-1.5 mb-6 text-[13.5px]/[1.5] text-tlf25-mut">Enter the 6-digit code we sent to your email.</p>
<div role="group" aria-label="6-digit verification code" class="grid grid-cols-6 gap-2 mb-6">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 1">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 2">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 3">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 4">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 5">
<input class="tlf-25-box min-w-0 h-13 max-[420px]:h-12 text-center text-[22px] max-[420px]:text-[19px] font-bold text-tlf25-ink rounded-xl bg-tlf25-field border-[1.5px] border-tlf25-fieldedge transition-[border-color,box-shadow,background-color,translate] duration-150 placeholder:text-tlf25-ph not-placeholder-shown:border-tlf25-acc not-placeholder-shown:bg-tlf25-tint focus-visible:outline-none focus-visible:border-tlf25-acc focus-visible:ring-4 focus-visible:ring-tlf25-halo focus-visible:-translate-y-0.5 motion-reduce:transition-colors motion-reduce:focus-visible:translate-y-0" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 6">
</div>
<button type="submit" class="w-full h-[50px] rounded-xl border-0 cursor-pointer bg-tlf25-acc text-white text-[15px] font-semibold transition-[filter,scale] duration-150 hover:brightness-110 active:scale-[0.99] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf25-ring motion-reduce:transition-none motion-reduce:active:scale-100">Verify</button>
<p class="mt-4 text-[13px] text-tlf25-mut">Didn’t get it? <a href="#" class="font-semibold text-tlf25-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tlf25-acc">Resend code</a></p>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tlf25-stage: #eef1f7;
--color-tlf25-stage: oklch(0.95 0.008 265);
--color-tlf25-edge: #e5e9f2;
--color-tlf25-edge: oklch(0.92 0.008 265);
--color-tlf25-fieldedge: #d5dbe8;
--color-tlf25-fieldedge: oklch(0.87 0.012 265);
--color-tlf25-field: #fafbfe;
--color-tlf25-field: oklch(0.99 0.003 265);
--color-tlf25-tint: #eef1fc;
--color-tlf25-tint: oklch(0.56 0.18 265 / 0.06);
--color-tlf25-ink: #1a2237;
--color-tlf25-ink: oklch(0.25 0.04 265);
--color-tlf25-mut: #666e88;
--color-tlf25-mut: oklch(0.52 0.03 265);
--color-tlf25-ph: #bcc3d4;
--color-tlf25-ph: oklch(0.8 0.015 265);
--color-tlf25-acc: #4159cd;
--color-tlf25-acc: oklch(0.51 0.18 265);
--color-tlf25-halo: #bfc9f0;
--color-tlf25-halo: oklch(0.51 0.18 265 / 0.2);
--color-tlf25-ring: #94a2ea;
--color-tlf25-ring: oklch(0.51 0.18 265 / 0.5);
--font-tlf25-sans: 'DM Sans', system-ui, sans-serif;
}
.tlf-25 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
const boxes = [...document.querySelectorAll('.tlf-25-box')];
boxes.forEach((box, i) => {
box.addEventListener('input', () => {
box.value = box.value.replace(/[^0-9]/g, '').slice(0, 1);
if (box.value && i < boxes.length - 1) boxes[i + 1].focus();
});
box.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !box.value && i > 0) boxes[i - 1].focus();
if (e.key === 'ArrowLeft' && i > 0) boxes[i - 1].focus();
if (e.key === 'ArrowRight' && i < boxes.length - 1) boxes[i + 1].focus();
});
box.addEventListener('paste', (e) => {
e.preventDefault();
const digits = (e.clipboardData.getData('text') || '').replace(/[^0-9]/g, '').split('');
boxes.forEach((b, j) => { if (digits[j] != null) b.value = digits[j]; });
boxes[Math.min(digits.length, boxes.length) - 1]?.focus();
});
});
```How this works
The script owns focus, CSS owns appearance. The handler moves focus on input, steps back on Backspace, and splits a pasted code across the boxes. It never adds a class — filled styling comes from whether the box has a value, which not-placeholder-shown: reads directly.
Paste is not optional. Users paste codes out of email. The paste handler strips non-digits, distributes what it finds, and focuses the last filled box; without it, this pattern is actively worse than a single text field.
The group announces as one field. role="group" with a label, plus per-box aria-label="Digit N" — so a screen reader says "6-digit verification code, Digit 1, edit text" rather than reading six unlabelled inputs.
Make it yours
- Code length: add boxes, change
grid-cols-6, and the script adapts (it reads the DOM). - Box size:
h-13andtext-[22px]; the narrow variant drops both viamax-[420px]:. - Accent:
--color-tlf25-accdrives focus, filled border and the tint. - Auto-submit on the final digit — three lines in the input handler.
Gotchas — read before shipping
- Support paste, and strip non-digits when you do.
autocomplete="one-time-code"belongs on the first box only; repeating it across all six confuses platform autofill.- Boxes need
min-w-0inside the grid, or the intrinsic input width blows the columns out on narrow screens.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Grid, :placeholder-shown and clipboard paste events are universally supported; the floor is Tailwind v4's generated sheet.