07 / 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 Passwordless Magic Link Login with data-state Machine
The hyper-minimalist single-field entry (Linear / Substack): one email input and a Send magic link button that swaps to a spinner and then a check-your-inbox confirmation, all at a locked height so nothing jumps. The port is the cleanest possible demonstration of group-data-[state=…]: — nineteen lines of attribute-selector CSS collapse into variants on the three elements that actually change.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tlf-07 w-full min-h-svh grid place-items-center bg-tlf07-stage font-tlf07-sans p-6">
<form novalidate data-state="idle" class="tlf-07-form group/form relative w-[min(380px,100%)] min-h-[280px] p-8 bg-tlf07-card border border-tlf07-edge rounded-[18px] overflow-hidden shadow-[0_30px_70px_-44px_black]">
<div class="flex flex-col gap-3.5 transition-[opacity,translate] duration-300 group-data-[state=sent]/form:opacity-0 group-data-[state=sent]/form:-translate-y-2.5 group-data-[state=sent]/form:pointer-events-none motion-reduce:transition-none">
<h1 class="text-2xl font-bold tracking-tight text-tlf07-ink">Sign in</h1>
<p class="mb-1 text-sm/[1.5] text-tlf07-mut">We’ll email you a magic link — no password needed.</p>
<label class="flex flex-col"><span class="sr-only">Email address</span>
<input type="email" name="email" autocomplete="email" inputmode="email" placeholder="you@company.com" required class="w-full h-[50px] px-4 rounded-xl bg-tlf07-field text-[15px] text-tlf07-ink border-[1.5px] border-tlf07-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf07-ph focus-visible:outline-none focus-visible:border-tlf07-acc focus-visible:ring-4 focus-visible:ring-tlf07-halo"></label>
<button type="submit" class="relative h-[50px] rounded-xl border-0 cursor-pointer overflow-hidden bg-tlf07-acc text-white text-[15px] font-semibold transition-[filter,scale] duration-150 hover:brightness-110 active:scale-[0.985] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-tlf07-ring group-data-[state=loading]/form:pointer-events-none after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:size-[18px] after:-mt-[9px] after:-ml-[9px] after:rounded-full after:border-[2.5px] after:border-white/40 after:border-t-white after:opacity-0 after:animate-tlf07-spin group-data-[state=loading]/form:after:opacity-100 motion-reduce:transition-none motion-reduce:after:animate-none">
<span class="transition-opacity duration-150 group-data-[state=loading]/form:opacity-0">Send magic link</span>
</button>
</div>
<div role="status" aria-live="polite" class="absolute inset-x-8 inset-y-8 flex flex-col items-center justify-center gap-3.5 text-center opacity-0 translate-y-2.5 pointer-events-none transition-[opacity,translate] duration-300 group-data-[state=sent]/form:opacity-100 group-data-[state=sent]/form:translate-y-0 group-data-[state=sent]/form:pointer-events-auto motion-reduce:transition-none">
<span aria-hidden="true" class="relative size-13 rounded-full bg-tlf07-ok after:content-[''] after:absolute after:left-[18px] after:top-[14px] after:w-3 after:h-[22px] after:border-tlf07-okink after:border-[0_3px_3px_0] after:rotate-[42deg]"></span>
<h2 class="text-xl font-bold text-tlf07-ink">Check your inbox</h2>
<p class="text-[13.5px]/[1.5] text-tlf07-mut">We sent a link to <b class="tlf-07-addr text-tlf07-strong">you@company.com</b>. It expires in 10 minutes.</p>
</div>
</form>
</section><section class="tlf-07 w-full min-h-svh grid place-items-center bg-tlf07-stage font-tlf07-sans p-6">
<form novalidate data-state="idle" class="tlf-07-form group/form relative w-[min(380px,100%)] min-h-[280px] p-8 bg-tlf07-card border border-tlf07-edge rounded-[18px] overflow-hidden shadow-[0_30px_70px_-44px_black]">
<div class="flex flex-col gap-3.5 transition-[opacity,translate] duration-300 group-data-[state=sent]/form:opacity-0 group-data-[state=sent]/form:-translate-y-2.5 group-data-[state=sent]/form:pointer-events-none motion-reduce:transition-none">
<h1 class="text-2xl font-bold tracking-tight text-tlf07-ink">Sign in</h1>
<p class="mb-1 text-sm/[1.5] text-tlf07-mut">We’ll email you a magic link — no password needed.</p>
<label class="flex flex-col"><span class="sr-only">Email address</span>
<input type="email" name="email" autocomplete="email" inputmode="email" placeholder="you@company.com" required class="w-full h-[50px] px-4 rounded-xl bg-tlf07-field text-[15px] text-tlf07-ink border-[1.5px] border-tlf07-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf07-ph focus-visible:outline-none focus-visible:border-tlf07-acc focus-visible:ring-4 focus-visible:ring-tlf07-halo"></label>
<button type="submit" class="relative h-[50px] rounded-xl border-0 cursor-pointer overflow-hidden bg-tlf07-acc text-white text-[15px] font-semibold transition-[filter,scale] duration-150 hover:brightness-110 active:scale-[0.985] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-tlf07-ring group-data-[state=loading]/form:pointer-events-none after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:size-[18px] after:-mt-[9px] after:-ml-[9px] after:rounded-full after:border-[2.5px] after:border-white/40 after:border-t-white after:opacity-0 after:animate-tlf07-spin group-data-[state=loading]/form:after:opacity-100 motion-reduce:transition-none motion-reduce:after:animate-none">
<span class="transition-opacity duration-150 group-data-[state=loading]/form:opacity-0">Send magic link</span>
</button>
</div>
<div role="status" aria-live="polite" class="absolute inset-x-8 inset-y-8 flex flex-col items-center justify-center gap-3.5 text-center opacity-0 translate-y-2.5 pointer-events-none transition-[opacity,translate] duration-300 group-data-[state=sent]/form:opacity-100 group-data-[state=sent]/form:translate-y-0 group-data-[state=sent]/form:pointer-events-auto motion-reduce:transition-none">
<span aria-hidden="true" class="relative size-13 rounded-full bg-tlf07-ok after:content-[''] after:absolute after:left-[18px] after:top-[14px] after:w-3 after:h-[22px] after:border-tlf07-okink after:border-[0_3px_3px_0] after:rotate-[42deg]"></span>
<h2 class="text-xl font-bold text-tlf07-ink">Check your inbox</h2>
<p class="text-[13.5px]/[1.5] text-tlf07-mut">We sent a link to <b class="tlf-07-addr text-tlf07-strong">you@company.com</b>. It expires in 10 minutes.</p>
</div>
</form>
</section>@import "tailwindcss";
@theme {
--color-tlf07-stage: #0f1117;
--color-tlf07-stage: oklch(0.17 0.012 265);
--color-tlf07-card: #171a23;
--color-tlf07-card: oklch(0.21 0.014 265);
--color-tlf07-edge: #262a37;
--color-tlf07-edge: oklch(0.28 0.016 265);
--color-tlf07-field: #11141c;
--color-tlf07-field: oklch(0.18 0.014 265);
--color-tlf07-fieldedge: #333a4c;
--color-tlf07-fieldedge: oklch(0.34 0.02 265);
--color-tlf07-ink: #f2f4fa;
--color-tlf07-ink: oklch(0.97 0.006 265);
--color-tlf07-strong: #cdd3e6;
--color-tlf07-strong: oklch(0.86 0.02 265);
--color-tlf07-mut: #a4abbf;
--color-tlf07-mut: oklch(0.74 0.02 265);
--color-tlf07-ph: #7c849c;
--color-tlf07-ph: oklch(0.6 0.02 265);
--color-tlf07-acc: #8a5cf6;
--color-tlf07-acc: oklch(0.6 0.2 280);
--color-tlf07-halo: #c3aefb;
--color-tlf07-halo: oklch(0.6 0.2 280 / 0.22);
--color-tlf07-ring: #b39cf9;
--color-tlf07-ring: oklch(0.6 0.2 280 / 0.55);
--color-tlf07-ok: #56d98c;
--color-tlf07-ok: oklch(0.78 0.17 155);
--color-tlf07-okink: #08210f;
--color-tlf07-okink: oklch(0.2 0.06 150);
--font-tlf07-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-tlf07-spin: tlf-07-spin 0.7s linear infinite;
}
@keyframes tlf-07-spin {
to {
transform: rotate(360deg);
}
}
.tlf-07 {
width: 100%;
min-height: 100svh;
color-scheme: dark;
}@import "tailwindcss";
@theme {
--color-tlf07-stage: #0f1117;
--color-tlf07-stage: oklch(0.17 0.012 265);
--color-tlf07-card: #171a23;
--color-tlf07-card: oklch(0.21 0.014 265);
--color-tlf07-edge: #262a37;
--color-tlf07-edge: oklch(0.28 0.016 265);
--color-tlf07-field: #11141c;
--color-tlf07-field: oklch(0.18 0.014 265);
--color-tlf07-fieldedge: #333a4c;
--color-tlf07-fieldedge: oklch(0.34 0.02 265);
--color-tlf07-ink: #f2f4fa;
--color-tlf07-ink: oklch(0.97 0.006 265);
--color-tlf07-strong: #cdd3e6;
--color-tlf07-strong: oklch(0.86 0.02 265);
--color-tlf07-mut: #a4abbf;
--color-tlf07-mut: oklch(0.74 0.02 265);
--color-tlf07-ph: #7c849c;
--color-tlf07-ph: oklch(0.6 0.02 265);
--color-tlf07-acc: #8a5cf6;
--color-tlf07-acc: oklch(0.6 0.2 280);
--color-tlf07-halo: #c3aefb;
--color-tlf07-halo: oklch(0.6 0.2 280 / 0.22);
--color-tlf07-ring: #b39cf9;
--color-tlf07-ring: oklch(0.6 0.2 280 / 0.55);
--color-tlf07-ok: #56d98c;
--color-tlf07-ok: oklch(0.78 0.17 155);
--color-tlf07-okink: #08210f;
--color-tlf07-okink: oklch(0.2 0.06 150);
--font-tlf07-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-tlf07-spin: tlf-07-spin 0.7s linear infinite;
}
@keyframes tlf-07-spin {
to {
transform: rotate(360deg);
}
}
.tlf-07 {
width: 100%;
min-height: 100svh;
color-scheme: dark;
}const form = document.querySelector('.tlf-07-form');
const email = form.querySelector('input[type=email]');
const btn = form.querySelector('button[type=submit]');
function send() {
if (form.dataset.state !== 'idle') return;
if (!email.checkValidity()) { email.reportValidity(); return; }
form.querySelector('.tlf-07-addr').textContent = email.value || 'you@company.com';
form.dataset.state = 'loading';
setTimeout(() => { form.dataset.state = 'sent'; }, 1400);
}
btn.addEventListener('click', (e) => { e.preventDefault(); send(); });
email.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } });const form = document.querySelector('.tlf-07-form');
const email = form.querySelector('input[type=email]');
const btn = form.querySelector('button[type=submit]');
function send() {
if (form.dataset.state !== 'idle') return;
if (!email.checkValidity()) { email.reportValidity(); return; }
form.querySelector('.tlf-07-addr').textContent = email.value || 'you@company.com';
form.dataset.state = 'loading';
setTimeout(() => { form.dataset.state = 'sent'; }, 1400);
}
btn.addEventListener('click', (e) => { e.preventDefault(); send(); });
email.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } });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-tlf07-stage: #0f1117;
--color-tlf07-stage: oklch(0.17 0.012 265);
--color-tlf07-card: #171a23;
--color-tlf07-card: oklch(0.21 0.014 265);
--color-tlf07-edge: #262a37;
--color-tlf07-edge: oklch(0.28 0.016 265);
--color-tlf07-field: #11141c;
--color-tlf07-field: oklch(0.18 0.014 265);
--color-tlf07-fieldedge: #333a4c;
--color-tlf07-fieldedge: oklch(0.34 0.02 265);
--color-tlf07-ink: #f2f4fa;
--color-tlf07-ink: oklch(0.97 0.006 265);
--color-tlf07-strong: #cdd3e6;
--color-tlf07-strong: oklch(0.86 0.02 265);
--color-tlf07-mut: #a4abbf;
--color-tlf07-mut: oklch(0.74 0.02 265);
--color-tlf07-ph: #7c849c;
--color-tlf07-ph: oklch(0.6 0.02 265);
--color-tlf07-acc: #8a5cf6;
--color-tlf07-acc: oklch(0.6 0.2 280);
--color-tlf07-halo: #c3aefb;
--color-tlf07-halo: oklch(0.6 0.2 280 / 0.22);
--color-tlf07-ring: #b39cf9;
--color-tlf07-ring: oklch(0.6 0.2 280 / 0.55);
--color-tlf07-ok: #56d98c;
--color-tlf07-ok: oklch(0.78 0.17 155);
--color-tlf07-okink: #08210f;
--color-tlf07-okink: oklch(0.2 0.06 150);
--font-tlf07-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-tlf07-spin: tlf-07-spin 0.7s linear infinite;
}
@keyframes tlf-07-spin {
to {
transform: rotate(360deg);
}
}
.tlf-07 {
width: 100%;
min-height: 100svh;
color-scheme: dark;
}@import "tailwindcss";
@theme {
--color-tlf07-stage: #0f1117;
--color-tlf07-stage: oklch(0.17 0.012 265);
--color-tlf07-card: #171a23;
--color-tlf07-card: oklch(0.21 0.014 265);
--color-tlf07-edge: #262a37;
--color-tlf07-edge: oklch(0.28 0.016 265);
--color-tlf07-field: #11141c;
--color-tlf07-field: oklch(0.18 0.014 265);
--color-tlf07-fieldedge: #333a4c;
--color-tlf07-fieldedge: oklch(0.34 0.02 265);
--color-tlf07-ink: #f2f4fa;
--color-tlf07-ink: oklch(0.97 0.006 265);
--color-tlf07-strong: #cdd3e6;
--color-tlf07-strong: oklch(0.86 0.02 265);
--color-tlf07-mut: #a4abbf;
--color-tlf07-mut: oklch(0.74 0.02 265);
--color-tlf07-ph: #7c849c;
--color-tlf07-ph: oklch(0.6 0.02 265);
--color-tlf07-acc: #8a5cf6;
--color-tlf07-acc: oklch(0.6 0.2 280);
--color-tlf07-halo: #c3aefb;
--color-tlf07-halo: oklch(0.6 0.2 280 / 0.22);
--color-tlf07-ring: #b39cf9;
--color-tlf07-ring: oklch(0.6 0.2 280 / 0.55);
--color-tlf07-ok: #56d98c;
--color-tlf07-ok: oklch(0.78 0.17 155);
--color-tlf07-okink: #08210f;
--color-tlf07-okink: oklch(0.2 0.06 150);
--font-tlf07-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-tlf07-spin: tlf-07-spin 0.7s linear infinite;
}
@keyframes tlf-07-spin {
to {
transform: rotate(360deg);
}
}
.tlf-07 {
width: 100%;
min-height: 100svh;
color-scheme: dark;
}<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=Plus+Jakarta+Sans:wght@400;500;600;700;800&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=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The JS is smaller after the port, because it no longer reaches for BEM class names it has to keep in sync — it sets one attribute and queries two hooks (
.tlf-07-form,.tlf-07-addr) that exist solely as JS handles. Keeping style classes and script hooks separate is a discipline Tailwind forces rather than suggests. --animate-tlf07-spinin@themeis the correct home for the spinner: the@keyframesstay in CSS (they must), but the composed animation shorthand becomes a utility.after:border-[0_3px_3px_0]ports the checkmark's asymmetric border trick verbatim — an arbitrary value carrying four sides in one utility, which reads better than the original'sborder-widthline.- UI change from the CSS original: muted copy lifted from
#9aa1b5tooklch(0.74 0.02 265)(6.9:1 on the card), the placeholder from#5f677eto0.6(4.6:1), and the success disc brightened so the dark checkmark inside it clears 4.5:1.
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 Passwordless Magic Link Login with data-state Machine
Source: https://codefronts.com/components/tailwind-css-login-forms/tailwind-passwordless-magic-link-login-data-state/
The hyper-minimalist single-field entry (Linear / Substack): one email input and a Send magic link button that swaps to a spinner and then a check-your-inbox confirmation, all at a locked height so nothing jumps. The port is the cleanest possible demonstration of group-data-[state=…]: — nineteen lines of attribute-selector CSS collapse into variants on the three elements that actually change.
## HTML
```html
<section class="tlf-07 w-full min-h-svh grid place-items-center bg-tlf07-stage font-tlf07-sans p-6">
<form novalidate data-state="idle" class="tlf-07-form group/form relative w-[min(380px,100%)] min-h-[280px] p-8 bg-tlf07-card border border-tlf07-edge rounded-[18px] overflow-hidden shadow-[0_30px_70px_-44px_black]">
<div class="flex flex-col gap-3.5 transition-[opacity,translate] duration-300 group-data-[state=sent]/form:opacity-0 group-data-[state=sent]/form:-translate-y-2.5 group-data-[state=sent]/form:pointer-events-none motion-reduce:transition-none">
<h1 class="text-2xl font-bold tracking-tight text-tlf07-ink">Sign in</h1>
<p class="mb-1 text-sm/[1.5] text-tlf07-mut">We’ll email you a magic link — no password needed.</p>
<label class="flex flex-col"><span class="sr-only">Email address</span>
<input type="email" name="email" autocomplete="email" inputmode="email" placeholder="you@company.com" required class="w-full h-[50px] px-4 rounded-xl bg-tlf07-field text-[15px] text-tlf07-ink border-[1.5px] border-tlf07-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf07-ph focus-visible:outline-none focus-visible:border-tlf07-acc focus-visible:ring-4 focus-visible:ring-tlf07-halo"></label>
<button type="submit" class="relative h-[50px] rounded-xl border-0 cursor-pointer overflow-hidden bg-tlf07-acc text-white text-[15px] font-semibold transition-[filter,scale] duration-150 hover:brightness-110 active:scale-[0.985] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-tlf07-ring group-data-[state=loading]/form:pointer-events-none after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:size-[18px] after:-mt-[9px] after:-ml-[9px] after:rounded-full after:border-[2.5px] after:border-white/40 after:border-t-white after:opacity-0 after:animate-tlf07-spin group-data-[state=loading]/form:after:opacity-100 motion-reduce:transition-none motion-reduce:after:animate-none">
<span class="transition-opacity duration-150 group-data-[state=loading]/form:opacity-0">Send magic link</span>
</button>
</div>
<div role="status" aria-live="polite" class="absolute inset-x-8 inset-y-8 flex flex-col items-center justify-center gap-3.5 text-center opacity-0 translate-y-2.5 pointer-events-none transition-[opacity,translate] duration-300 group-data-[state=sent]/form:opacity-100 group-data-[state=sent]/form:translate-y-0 group-data-[state=sent]/form:pointer-events-auto motion-reduce:transition-none">
<span aria-hidden="true" class="relative size-13 rounded-full bg-tlf07-ok after:content-[''] after:absolute after:left-[18px] after:top-[14px] after:w-3 after:h-[22px] after:border-tlf07-okink after:border-[0_3px_3px_0] after:rotate-[42deg]"></span>
<h2 class="text-xl font-bold text-tlf07-ink">Check your inbox</h2>
<p class="text-[13.5px]/[1.5] text-tlf07-mut">We sent a link to <b class="tlf-07-addr text-tlf07-strong">you@company.com</b>. It expires in 10 minutes.</p>
</div>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tlf07-stage: #0f1117;
--color-tlf07-stage: oklch(0.17 0.012 265);
--color-tlf07-card: #171a23;
--color-tlf07-card: oklch(0.21 0.014 265);
--color-tlf07-edge: #262a37;
--color-tlf07-edge: oklch(0.28 0.016 265);
--color-tlf07-field: #11141c;
--color-tlf07-field: oklch(0.18 0.014 265);
--color-tlf07-fieldedge: #333a4c;
--color-tlf07-fieldedge: oklch(0.34 0.02 265);
--color-tlf07-ink: #f2f4fa;
--color-tlf07-ink: oklch(0.97 0.006 265);
--color-tlf07-strong: #cdd3e6;
--color-tlf07-strong: oklch(0.86 0.02 265);
--color-tlf07-mut: #a4abbf;
--color-tlf07-mut: oklch(0.74 0.02 265);
--color-tlf07-ph: #7c849c;
--color-tlf07-ph: oklch(0.6 0.02 265);
--color-tlf07-acc: #8a5cf6;
--color-tlf07-acc: oklch(0.6 0.2 280);
--color-tlf07-halo: #c3aefb;
--color-tlf07-halo: oklch(0.6 0.2 280 / 0.22);
--color-tlf07-ring: #b39cf9;
--color-tlf07-ring: oklch(0.6 0.2 280 / 0.55);
--color-tlf07-ok: #56d98c;
--color-tlf07-ok: oklch(0.78 0.17 155);
--color-tlf07-okink: #08210f;
--color-tlf07-okink: oklch(0.2 0.06 150);
--font-tlf07-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-tlf07-spin: tlf-07-spin 0.7s linear infinite;
}
@keyframes tlf-07-spin {
to {
transform: rotate(360deg);
}
}
.tlf-07 {
width: 100%;
min-height: 100svh;
color-scheme: dark;
}
```
## JavaScript
```js
const form = document.querySelector('.tlf-07-form');
const email = form.querySelector('input[type=email]');
const btn = form.querySelector('button[type=submit]');
function send() {
if (form.dataset.state !== 'idle') return;
if (!email.checkValidity()) { email.reportValidity(); return; }
form.querySelector('.tlf-07-addr').textContent = email.value || 'you@company.com';
form.dataset.state = 'loading';
setTimeout(() => { form.dataset.state = 'sent'; }, 1400);
}
btn.addEventListener('click', (e) => { e.preventDefault(); send(); });
email.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } });
```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 Passwordless Magic Link Login with data-state Machine
Source: https://codefronts.com/components/tailwind-css-login-forms/tailwind-passwordless-magic-link-login-data-state/
The hyper-minimalist single-field entry (Linear / Substack): one email input and a Send magic link button that swaps to a spinner and then a check-your-inbox confirmation, all at a locked height so nothing jumps. The port is the cleanest possible demonstration of group-data-[state=…]: — nineteen lines of attribute-selector CSS collapse into variants on the three elements that actually change.
## HTML
```html
<section class="tlf-07 w-full min-h-svh grid place-items-center bg-tlf07-stage font-tlf07-sans p-6">
<form novalidate data-state="idle" class="tlf-07-form group/form relative w-[min(380px,100%)] min-h-[280px] p-8 bg-tlf07-card border border-tlf07-edge rounded-[18px] overflow-hidden shadow-[0_30px_70px_-44px_black]">
<div class="flex flex-col gap-3.5 transition-[opacity,translate] duration-300 group-data-[state=sent]/form:opacity-0 group-data-[state=sent]/form:-translate-y-2.5 group-data-[state=sent]/form:pointer-events-none motion-reduce:transition-none">
<h1 class="text-2xl font-bold tracking-tight text-tlf07-ink">Sign in</h1>
<p class="mb-1 text-sm/[1.5] text-tlf07-mut">We’ll email you a magic link — no password needed.</p>
<label class="flex flex-col"><span class="sr-only">Email address</span>
<input type="email" name="email" autocomplete="email" inputmode="email" placeholder="you@company.com" required class="w-full h-[50px] px-4 rounded-xl bg-tlf07-field text-[15px] text-tlf07-ink border-[1.5px] border-tlf07-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf07-ph focus-visible:outline-none focus-visible:border-tlf07-acc focus-visible:ring-4 focus-visible:ring-tlf07-halo"></label>
<button type="submit" class="relative h-[50px] rounded-xl border-0 cursor-pointer overflow-hidden bg-tlf07-acc text-white text-[15px] font-semibold transition-[filter,scale] duration-150 hover:brightness-110 active:scale-[0.985] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-tlf07-ring group-data-[state=loading]/form:pointer-events-none after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:size-[18px] after:-mt-[9px] after:-ml-[9px] after:rounded-full after:border-[2.5px] after:border-white/40 after:border-t-white after:opacity-0 after:animate-tlf07-spin group-data-[state=loading]/form:after:opacity-100 motion-reduce:transition-none motion-reduce:after:animate-none">
<span class="transition-opacity duration-150 group-data-[state=loading]/form:opacity-0">Send magic link</span>
</button>
</div>
<div role="status" aria-live="polite" class="absolute inset-x-8 inset-y-8 flex flex-col items-center justify-center gap-3.5 text-center opacity-0 translate-y-2.5 pointer-events-none transition-[opacity,translate] duration-300 group-data-[state=sent]/form:opacity-100 group-data-[state=sent]/form:translate-y-0 group-data-[state=sent]/form:pointer-events-auto motion-reduce:transition-none">
<span aria-hidden="true" class="relative size-13 rounded-full bg-tlf07-ok after:content-[''] after:absolute after:left-[18px] after:top-[14px] after:w-3 after:h-[22px] after:border-tlf07-okink after:border-[0_3px_3px_0] after:rotate-[42deg]"></span>
<h2 class="text-xl font-bold text-tlf07-ink">Check your inbox</h2>
<p class="text-[13.5px]/[1.5] text-tlf07-mut">We sent a link to <b class="tlf-07-addr text-tlf07-strong">you@company.com</b>. It expires in 10 minutes.</p>
</div>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tlf07-stage: #0f1117;
--color-tlf07-stage: oklch(0.17 0.012 265);
--color-tlf07-card: #171a23;
--color-tlf07-card: oklch(0.21 0.014 265);
--color-tlf07-edge: #262a37;
--color-tlf07-edge: oklch(0.28 0.016 265);
--color-tlf07-field: #11141c;
--color-tlf07-field: oklch(0.18 0.014 265);
--color-tlf07-fieldedge: #333a4c;
--color-tlf07-fieldedge: oklch(0.34 0.02 265);
--color-tlf07-ink: #f2f4fa;
--color-tlf07-ink: oklch(0.97 0.006 265);
--color-tlf07-strong: #cdd3e6;
--color-tlf07-strong: oklch(0.86 0.02 265);
--color-tlf07-mut: #a4abbf;
--color-tlf07-mut: oklch(0.74 0.02 265);
--color-tlf07-ph: #7c849c;
--color-tlf07-ph: oklch(0.6 0.02 265);
--color-tlf07-acc: #8a5cf6;
--color-tlf07-acc: oklch(0.6 0.2 280);
--color-tlf07-halo: #c3aefb;
--color-tlf07-halo: oklch(0.6 0.2 280 / 0.22);
--color-tlf07-ring: #b39cf9;
--color-tlf07-ring: oklch(0.6 0.2 280 / 0.55);
--color-tlf07-ok: #56d98c;
--color-tlf07-ok: oklch(0.78 0.17 155);
--color-tlf07-okink: #08210f;
--color-tlf07-okink: oklch(0.2 0.06 150);
--font-tlf07-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-tlf07-spin: tlf-07-spin 0.7s linear infinite;
}
@keyframes tlf-07-spin {
to {
transform: rotate(360deg);
}
}
.tlf-07 {
width: 100%;
min-height: 100svh;
color-scheme: dark;
}
```
## JavaScript
```js
const form = document.querySelector('.tlf-07-form');
const email = form.querySelector('input[type=email]');
const btn = form.querySelector('button[type=submit]');
function send() {
if (form.dataset.state !== 'idle') return;
if (!email.checkValidity()) { email.reportValidity(); return; }
form.querySelector('.tlf-07-addr').textContent = email.value || 'you@company.com';
form.dataset.state = 'loading';
setTimeout(() => { form.dataset.state = 'sent'; }, 1400);
}
btn.addEventListener('click', (e) => { e.preventDefault(); send(); });
email.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } });
```How this works
One attribute drives three elements. JS sets data-state to idle, loading or sent and touches nothing else. The button label fades at loading, the spinner pseudo-element appears at loading, the idle stack fades out and the success panel fades in at sent — each stated on its own element as a group-data-* variant instead of four descendant selectors in a stylesheet.
The spinner is a pseudo-element, still. after:size-[18px] after:rounded-full after:border-[2.5px] after:border-white/40 after:border-t-white plus the themed animation: no extra DOM node, and the ring is hidden at opacity-0 rather than removed, so no layout is involved in showing it.
Height is locked, not animated. Both states are absolutely stacked inside a min-h card, cross-fading with opacity and a 10px translate. Nothing about the swap can move the page, which is the entire reason this pattern survives a Core Web Vitals audit.
Make it yours
- Point the timeout at a real endpoint; keep the three
data-statevalues and the CSS is unchanged. - Accent:
--color-tlf07-accdrives button, focus halo and spinner tint. - Card height:
min-h-[280px]— raise it if you add a sub-label so the idle state never scrolls. - Success glyph and copy live in the one
role="status"block.
Gotchas — read before shipping
- Lock the container height. Every version of this pattern that animates height instead of cross-fading ships a visible layout shift on submit.
group-data-[state=loading]/form:needs the named group (group/form) once a second group could nest — unnamed groups resolve to the nearest ancestor, which is a bug that appears only after someone wraps the form.- Block double-sends in the handler (the
state !== 'idle'guard), not withpointer-events-nonealone — Enter on the input bypasses pointer events entirely.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Data-attribute variants and @keyframes are universally supported; the floor is Tailwind v4's generated sheet.