30 Tailwind CSS Login Forms
22 / 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
Pure TailwindMIT licensed

Tailwind Secure Vault Login with user-invalid Error State

An ironclad industrial login for banking and secure-storage: flat steel tones, geometric borders, a lock crest and a crimson error state that shakes once when a field is left invalid. The port upgrades the validation itself — user-invalid: replaces the original's :invalid:not(:focus):not(:placeholder-shown) stack and only fires after the user has actually interacted.

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-22 w-full min-h-svh grid place-items-center bg-tlf22-stage font-tlf22-sans p-6">
  <form novalidate class="w-[min(390px,100%)] flex flex-col gap-4 p-8 bg-tlf22-steel border border-tlf22-line rounded-xl shadow-[0_30px_70px_-40px_black,inset_0_1px_0_oklch(1_0_0/0.05)]">
    <div aria-hidden="true" class="grid place-items-center size-13 rounded-[10px] text-2xl bg-tlf22-crest border-2 border-tlf22-line">&#128274;</div>
    <h1 class="text-xl font-bold text-tlf22-ink">Secure Vault Access</h1>
    <p class="-mt-2.5 text-[12.5px] uppercase tracking-[0.06em] text-tlf22-mut">Authorised personnel only.</p>
    <div class="flex flex-col gap-1.5">
      <label for="tlf-22-acct" class="text-xs font-bold uppercase tracking-[0.05em] text-tlf22-label">Account ID</label>
      <input id="tlf-22-acct" type="text" inputmode="numeric" pattern="[0-9]{6,}" placeholder="000000" required class="h-12 px-3.5 rounded-md font-tlf22-mono text-[15px] tracking-[0.08em] text-tlf22-ink bg-tlf22-field border-[1.5px] border-tlf22-line transition-[border-color,box-shadow] duration-150 placeholder:text-tlf22-ph placeholder:tracking-[0.15em] focus-visible:outline-none focus-visible:border-tlf22-acc focus-visible:ring-3 focus-visible:ring-tlf22-halo user-invalid:border-tlf22-danger user-invalid:ring-3 user-invalid:ring-tlf22-dangerhalo user-invalid:animate-tlf22-shake motion-reduce:user-invalid:animate-none">
      <div class="h-0 opacity-0 overflow-hidden transition-opacity duration-200 has-user-invalid:h-auto has-user-invalid:opacity-100"><span role="alert" aria-live="polite" class="text-xs font-semibold text-tlf22-danger">Enter a valid 6+ digit account ID.</span></div>
    </div>
    <div class="flex flex-col gap-1.5">
      <label for="tlf-22-pin" class="text-xs font-bold uppercase tracking-[0.05em] text-tlf22-label">Passphrase</label>
      <input id="tlf-22-pin" type="password" autocomplete="current-password" placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;" required minlength="8" class="h-12 px-3.5 rounded-md font-tlf22-mono text-[15px] tracking-[0.08em] text-tlf22-ink bg-tlf22-field border-[1.5px] border-tlf22-line transition-[border-color,box-shadow] duration-150 placeholder:text-tlf22-ph placeholder:tracking-[0.15em] focus-visible:outline-none focus-visible:border-tlf22-acc focus-visible:ring-3 focus-visible:ring-tlf22-halo user-invalid:border-tlf22-danger user-invalid:ring-3 user-invalid:ring-tlf22-dangerhalo user-invalid:animate-tlf22-shake motion-reduce:user-invalid:animate-none">
      <div class="h-0 opacity-0 overflow-hidden transition-opacity duration-200 has-user-invalid:h-auto has-user-invalid:opacity-100"><span role="alert" aria-live="polite" class="text-xs font-semibold text-tlf22-danger">Passphrase must be at least 8 characters.</span></div>
    </div>
    <button type="submit" class="h-12 mt-1 rounded-md cursor-pointer bg-tlf22-btn border border-tlf22-acc text-tlf22-btnink text-sm font-bold uppercase tracking-[0.08em] transition-[background-color,scale] duration-150 hover:bg-tlf22-btnhover active:scale-[0.985] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf22-ring motion-reduce:transition-colors motion-reduce:active:scale-100">Unlock</button>
  </form>
</section>
@import "tailwindcss";

@theme {
  --color-tlf22-stage: #0d1015;
  --color-tlf22-stage: oklch(0.15 0.008 250);
  --color-tlf22-steel: #1c222c;
  --color-tlf22-steel: oklch(0.23 0.012 250);
  --color-tlf22-crest: #141922;
  --color-tlf22-crest: oklch(0.19 0.012 250);
  --color-tlf22-field: #12161e;
  --color-tlf22-field: oklch(0.18 0.012 250);
  --color-tlf22-line: #3a4452;
  --color-tlf22-line: oklch(0.34 0.018 250);
  --color-tlf22-ink: #e8edf3;
  --color-tlf22-ink: oklch(0.94 0.008 250);
  --color-tlf22-label: #a8b3c4;
  --color-tlf22-label: oklch(0.76 0.018 250);
  --color-tlf22-mut: #8e9aab;
  --color-tlf22-mut: oklch(0.67 0.02 250);
  --color-tlf22-ph: #6b7688;
  --color-tlf22-ph: oklch(0.55 0.02 250);
  --color-tlf22-acc: #5ec4e0;
  --color-tlf22-acc: oklch(0.76 0.13 210);
  --color-tlf22-halo: #a6dcec;
  --color-tlf22-halo: oklch(0.76 0.13 210 / 0.25);
  --color-tlf22-ring: #8ed3e7;
  --color-tlf22-ring: oklch(0.76 0.13 210 / 0.55);
  --color-tlf22-btn: #14323d;
  --color-tlf22-btn: oklch(0.76 0.13 210 / 0.16);
  --color-tlf22-btnhover: #1a4453;
  --color-tlf22-btnhover: oklch(0.76 0.13 210 / 0.26);
  --color-tlf22-btnink: #dff2fb;
  --color-tlf22-btnink: oklch(0.95 0.03 210);
  --color-tlf22-danger: #f28b7d;
  --color-tlf22-danger: oklch(0.72 0.16 25);
  --color-tlf22-dangerhalo: #e97a6a;
  --color-tlf22-dangerhalo: oklch(0.72 0.16 25 / 0.22);
  --font-tlf22-sans: 'IBM Plex Sans', system-ui, sans-serif;
  --font-tlf22-mono: 'IBM Plex Mono', ui-monospace, Menlo, monospace;
  --animate-tlf22-shake: tlf-22-shake 0.4s ease-in-out 1;
}

@keyframes tlf-22-shake {
  0%,
    100% {
    transform: translateX(0);
  }

  20% {
    transform: translateX(-6px);
  }

  40% {
    transform: translateX(6px);
  }

  60% {
    transform: translateX(-4px);
  }

  80% {
    transform: translateX(4px);
  }
}

.tlf-22 {
  width: 100%;
  min-height: 100svh;
  color-scheme: dark;
}

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-tlf22-stage: #0d1015;
  --color-tlf22-stage: oklch(0.15 0.008 250);
  --color-tlf22-steel: #1c222c;
  --color-tlf22-steel: oklch(0.23 0.012 250);
  --color-tlf22-crest: #141922;
  --color-tlf22-crest: oklch(0.19 0.012 250);
  --color-tlf22-field: #12161e;
  --color-tlf22-field: oklch(0.18 0.012 250);
  --color-tlf22-line: #3a4452;
  --color-tlf22-line: oklch(0.34 0.018 250);
  --color-tlf22-ink: #e8edf3;
  --color-tlf22-ink: oklch(0.94 0.008 250);
  --color-tlf22-label: #a8b3c4;
  --color-tlf22-label: oklch(0.76 0.018 250);
  --color-tlf22-mut: #8e9aab;
  --color-tlf22-mut: oklch(0.67 0.02 250);
  --color-tlf22-ph: #6b7688;
  --color-tlf22-ph: oklch(0.55 0.02 250);
  --color-tlf22-acc: #5ec4e0;
  --color-tlf22-acc: oklch(0.76 0.13 210);
  --color-tlf22-halo: #a6dcec;
  --color-tlf22-halo: oklch(0.76 0.13 210 / 0.25);
  --color-tlf22-ring: #8ed3e7;
  --color-tlf22-ring: oklch(0.76 0.13 210 / 0.55);
  --color-tlf22-btn: #14323d;
  --color-tlf22-btn: oklch(0.76 0.13 210 / 0.16);
  --color-tlf22-btnhover: #1a4453;
  --color-tlf22-btnhover: oklch(0.76 0.13 210 / 0.26);
  --color-tlf22-btnink: #dff2fb;
  --color-tlf22-btnink: oklch(0.95 0.03 210);
  --color-tlf22-danger: #f28b7d;
  --color-tlf22-danger: oklch(0.72 0.16 25);
  --color-tlf22-dangerhalo: #e97a6a;
  --color-tlf22-dangerhalo: oklch(0.72 0.16 25 / 0.22);
  --font-tlf22-sans: 'IBM Plex Sans', system-ui, sans-serif;
  --font-tlf22-mono: 'IBM Plex Mono', ui-monospace, Menlo, monospace;
  --animate-tlf22-shake: tlf-22-shake 0.4s ease-in-out 1;
}

@keyframes tlf-22-shake {
  0%,
    100% {
    transform: translateX(0);
  }

  20% {
    transform: translateX(-6px);
  }

  40% {
    transform: translateX(6px);
  }

  60% {
    transform: translateX(-4px);
  }

  80% {
    transform: translateX(4px);
  }
}

.tlf-22 {
  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=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap">

What changed in the port

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

  • user-invalid: is the single best argument in this collection for re-porting an old form: three chained pseudo-classes that approximated "the user got this wrong" become one variant that means exactly that, and the timing is the browser's problem rather than yours.
  • has-user-invalid: on the message wrapper is :has(:user-invalid) — the error text reacts to its sibling field without either element knowing a class name.
  • motion-reduce:user-invalid:animate-none stacks two variants in the order Tailwind expects (media condition outermost); reversing them compiles to a rule that never matches.
  • UI change from the CSS original: IBM Plex Sans/Mono replace the system stacks, the crimson lifted from oklch(0.62 0.2 25) (3.9:1 on steel) to 0.72 0.16 25 (6.4:1), the steel hairline from #333c49 to oklch(0.34) so field edges are visible, and the error messages became live regions rather than role="alert" spans that were already in the DOM at opacity:0 with no live semantics.
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 Secure Vault Login with user-invalid Error State
Source: https://codefronts.com/components/tailwind-css-login-forms/tailwind-secure-vault-login-user-invalid-error/

An ironclad industrial login for banking and secure-storage: flat steel tones, geometric borders, a lock crest and a crimson error state that shakes once when a field is left invalid. The port upgrades the validation itself &mdash; user-invalid: replaces the original's :invalid:not(:focus):not(:placeholder-shown) stack and only fires after the user has actually interacted.
## HTML
```html
<section class="tlf-22 w-full min-h-svh grid place-items-center bg-tlf22-stage font-tlf22-sans p-6">
  <form novalidate class="w-[min(390px,100%)] flex flex-col gap-4 p-8 bg-tlf22-steel border border-tlf22-line rounded-xl shadow-[0_30px_70px_-40px_black,inset_0_1px_0_oklch(1_0_0/0.05)]">
    <div aria-hidden="true" class="grid place-items-center size-13 rounded-[10px] text-2xl bg-tlf22-crest border-2 border-tlf22-line">&#128274;</div>
    <h1 class="text-xl font-bold text-tlf22-ink">Secure Vault Access</h1>
    <p class="-mt-2.5 text-[12.5px] uppercase tracking-[0.06em] text-tlf22-mut">Authorised personnel only.</p>
    <div class="flex flex-col gap-1.5">
      <label for="tlf-22-acct" class="text-xs font-bold uppercase tracking-[0.05em] text-tlf22-label">Account ID</label>
      <input id="tlf-22-acct" type="text" inputmode="numeric" pattern="[0-9]{6,}" placeholder="000000" required class="h-12 px-3.5 rounded-md font-tlf22-mono text-[15px] tracking-[0.08em] text-tlf22-ink bg-tlf22-field border-[1.5px] border-tlf22-line transition-[border-color,box-shadow] duration-150 placeholder:text-tlf22-ph placeholder:tracking-[0.15em] focus-visible:outline-none focus-visible:border-tlf22-acc focus-visible:ring-3 focus-visible:ring-tlf22-halo user-invalid:border-tlf22-danger user-invalid:ring-3 user-invalid:ring-tlf22-dangerhalo user-invalid:animate-tlf22-shake motion-reduce:user-invalid:animate-none">
      <div class="h-0 opacity-0 overflow-hidden transition-opacity duration-200 has-user-invalid:h-auto has-user-invalid:opacity-100"><span role="alert" aria-live="polite" class="text-xs font-semibold text-tlf22-danger">Enter a valid 6+ digit account ID.</span></div>
    </div>
    <div class="flex flex-col gap-1.5">
      <label for="tlf-22-pin" class="text-xs font-bold uppercase tracking-[0.05em] text-tlf22-label">Passphrase</label>
      <input id="tlf-22-pin" type="password" autocomplete="current-password" placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;" required minlength="8" class="h-12 px-3.5 rounded-md font-tlf22-mono text-[15px] tracking-[0.08em] text-tlf22-ink bg-tlf22-field border-[1.5px] border-tlf22-line transition-[border-color,box-shadow] duration-150 placeholder:text-tlf22-ph placeholder:tracking-[0.15em] focus-visible:outline-none focus-visible:border-tlf22-acc focus-visible:ring-3 focus-visible:ring-tlf22-halo user-invalid:border-tlf22-danger user-invalid:ring-3 user-invalid:ring-tlf22-dangerhalo user-invalid:animate-tlf22-shake motion-reduce:user-invalid:animate-none">
      <div class="h-0 opacity-0 overflow-hidden transition-opacity duration-200 has-user-invalid:h-auto has-user-invalid:opacity-100"><span role="alert" aria-live="polite" class="text-xs font-semibold text-tlf22-danger">Passphrase must be at least 8 characters.</span></div>
    </div>
    <button type="submit" class="h-12 mt-1 rounded-md cursor-pointer bg-tlf22-btn border border-tlf22-acc text-tlf22-btnink text-sm font-bold uppercase tracking-[0.08em] transition-[background-color,scale] duration-150 hover:bg-tlf22-btnhover active:scale-[0.985] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf22-ring motion-reduce:transition-colors motion-reduce:active:scale-100">Unlock</button>
  </form>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tlf22-stage: #0d1015;
  --color-tlf22-stage: oklch(0.15 0.008 250);
  --color-tlf22-steel: #1c222c;
  --color-tlf22-steel: oklch(0.23 0.012 250);
  --color-tlf22-crest: #141922;
  --color-tlf22-crest: oklch(0.19 0.012 250);
  --color-tlf22-field: #12161e;
  --color-tlf22-field: oklch(0.18 0.012 250);
  --color-tlf22-line: #3a4452;
  --color-tlf22-line: oklch(0.34 0.018 250);
  --color-tlf22-ink: #e8edf3;
  --color-tlf22-ink: oklch(0.94 0.008 250);
  --color-tlf22-label: #a8b3c4;
  --color-tlf22-label: oklch(0.76 0.018 250);
  --color-tlf22-mut: #8e9aab;
  --color-tlf22-mut: oklch(0.67 0.02 250);
  --color-tlf22-ph: #6b7688;
  --color-tlf22-ph: oklch(0.55 0.02 250);
  --color-tlf22-acc: #5ec4e0;
  --color-tlf22-acc: oklch(0.76 0.13 210);
  --color-tlf22-halo: #a6dcec;
  --color-tlf22-halo: oklch(0.76 0.13 210 / 0.25);
  --color-tlf22-ring: #8ed3e7;
  --color-tlf22-ring: oklch(0.76 0.13 210 / 0.55);
  --color-tlf22-btn: #14323d;
  --color-tlf22-btn: oklch(0.76 0.13 210 / 0.16);
  --color-tlf22-btnhover: #1a4453;
  --color-tlf22-btnhover: oklch(0.76 0.13 210 / 0.26);
  --color-tlf22-btnink: #dff2fb;
  --color-tlf22-btnink: oklch(0.95 0.03 210);
  --color-tlf22-danger: #f28b7d;
  --color-tlf22-danger: oklch(0.72 0.16 25);
  --color-tlf22-dangerhalo: #e97a6a;
  --color-tlf22-dangerhalo: oklch(0.72 0.16 25 / 0.22);
  --font-tlf22-sans: 'IBM Plex Sans', system-ui, sans-serif;
  --font-tlf22-mono: 'IBM Plex Mono', ui-monospace, Menlo, monospace;
  --animate-tlf22-shake: tlf-22-shake 0.4s ease-in-out 1;
}

@keyframes tlf-22-shake {
  0%,
    100% {
    transform: translateX(0);
  }

  20% {
    transform: translateX(-6px);
  }

  40% {
    transform: translateX(6px);
  }

  60% {
    transform: translateX(-4px);
  }

  80% {
    transform: translateX(4px);
  }
}

.tlf-22 {
  width: 100%;
  min-height: 100svh;
  color-scheme: dark;
}
```

How this works

user-invalid is the real fix. :invalid matches an empty required field before the user has typed anything, which is why the CSS original needed :not(:focus):not(:placeholder-shown) to avoid shouting at people on page load. :user-invalid is specified to match only after interaction, so the port's condition is one variant and is also more correct.

The shake is one transform animation. 400ms, five keyframes, translateX only, played once. Under motion-reduce: it does not play at all — the crimson border and the announced message carry the information, which is the point of the fallback.

Error copy is revealed, not inserted. The message is always in the DOM at h-0 opacity-0 and expands under has-user-invalid:, so screen readers see a live region that already exists rather than a node appearing mid-announcement.

Make it yours

  • Steel and crimson: --color-tlf22-steel, --color-tlf22-danger.
  • Shake intensity: the @keyframes offsets.
  • Driving errors from JS instead? Swap user-invalid: for data-[error]: and set the attribute — the rest of the class list is unchanged.
  • The crest is a glyph in a bordered box; drop your brand mark in its place.

Gotchas — read before shipping

  • Don't use bare invalid: in Tailwind for this. It matches before first input and the form screams at a user who has done nothing.
  • Error messages need role="alert" — colour is not an announcement.
  • Keep crimson at AA against the steel surface (oklch(0.68 0.19 25) here, not a deep red), or the warning is the least readable text on the screen.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

:user-invalid is Chrome 119, Safari 16.5, Firefox 88. Older engines simply never match the error variants, leaving the field in its resting state — no false positives.

Techniques used in this demo

Search CodeFronts

Loading…