30 Tailwind CSS Login Forms
07 / 30

All 30 in 30 Tailwind CSS Login Forms

  1. 01Tailwind Split-Screen Brand Hero Login Form
  2. 02Tailwind Multi-Tenant Workspace Account Switcher
  3. 03Tailwind Floating Label Login Form with peer Variants
  4. 04Tailwind Web3 Wallet Connect Login Grid
  5. 05Tailwind Minimalist Fintech Login Card with Layered Shadow
  6. 06Tailwind OAuth Social Login Form with Brand SVG Buttons
  7. 07Tailwind Passwordless Magic Link Login with data-state Machine
  8. 08Tailwind Aurora Glow Login Card with Drifting Blur Blobs
  9. 09Tailwind Neon Synthwave Arcade Login Form
  10. 10Tailwind Sliding Login and Sign Up Panel with peer-checked
  11. 11Tailwind 3D Flip Login Card with Password Reset Back Face
  12. 12Tailwind Terminal CLI Prompt Login Form
  13. 13Tailwind Floating Pill Login Widget Docked Over App Content
  14. 14Tailwind Gradient Border Login Card with background-clip
  15. 15Tailwind Slide-Over Side Panel Login without JavaScript
  16. 16Tailwind Social-First Sign Up with Brand-Compliant Buttons
  17. 17Tailwind Animated Mascot Login that Covers Its Eyes
  18. 18Tailwind Accessible Compact Login Modal with Scale-In
  19. 19Tailwind Glassmorphism Login Card with backdrop-filter
  20. 20Tailwind Multi-Step Login Wizard with Radio-Driven Track
  21. 21Tailwind Animated Conic Gradient Border Login Card
  22. 22Tailwind Secure Vault Login with user-invalid Error State
  23. 23Tailwind Neo-Brutalist Login Form with Hard Offset Shadow
  24. 24Tailwind Show Hide Password Toggle with aria-pressed
  25. 25Tailwind OTP Code Input Grid with Auto-Advance
  26. 26Tailwind Password Strength Meter with data-score Variants
  27. 27Tailwind Numeric PIN Pad Login for Kiosks and POS
  28. 28Tailwind Inline Form Validation with user-valid and user-invalid
  29. 29Tailwind Particle Constellation Login over Canvas
  30. 30Tailwind Biometric Face ID Prompt with Pulse Ripples
Tailwind + JSMIT licensed

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.

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

<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&rsquo;ll email you a magic link &mdash; 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;
}
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.

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-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;
}
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=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-spin in @theme is the correct home for the spinner: the @keyframes stay 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's border-width line.
  • UI change from the CSS original: muted copy lifted from #9aa1b5 to oklch(0.74 0.02 265) (6.9:1 on the card), the placeholder from #5f677e to 0.6 (4.6:1), and the success disc brightened so the dark checkmark inside it clears 4.5:1.
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 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=…]: &mdash; 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&rsquo;ll email you a magic link &mdash; 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-state values and the CSS is unchanged.
  • Accent: --color-tlf07-acc drives 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 with pointer-events-none alone — Enter on the input bypasses pointer events entirely.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Data-attribute variants and @keyframes are universally supported; the floor is Tailwind v4's generated sheet.

Techniques used in this demo

Utilitiesfirst-class utility or variant

HTML & JSno Tailwind involved

Search CodeFronts

Loading…