17 / 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 Animated Mascot Login that Covers Its Eyes
A character sits above the form and reacts to the user: eyes glance down at the email field, then hands shoot up to cover them the moment the password field is focused. The port is group-has-*'s best demo in the collection — the CSS original's .card:has(.field--pw:focus-within) .arm becomes a variant on the arm itself.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tlf-17 w-full min-h-svh grid place-items-center bg-tlf17-stage font-tlf17-sans p-6">
<form novalidate class="group/card relative w-[min(360px,100%)] flex flex-col gap-3.5 pt-16 px-8 pb-8 bg-white border border-tlf17-edge rounded-[20px] shadow-[0_24px_56px_-38px_oklch(0.4_0.08_50/0.5)]">
<div aria-hidden="true" class="absolute -top-12 left-1/2 -translate-x-1/2 size-24">
<svg viewBox="0 0 120 120" class="w-full h-full overflow-visible">
<circle cx="60" cy="60" r="44" class="fill-tlf17-fur"></circle>
<circle cx="30" cy="26" r="12" class="fill-tlf17-fur2"></circle>
<circle cx="90" cy="26" r="12" class="fill-tlf17-fur2"></circle>
<circle cx="46" cy="56" r="7" class="tlf-17-eye fill-tlf17-eye origin-center transition-transform duration-200 group-has-[.tlf-17-email:focus-within]/card:translate-y-[3px] group-has-[.tlf-17-pw:focus-within]/card:scale-y-[0.15] motion-reduce:transition-none"></circle>
<circle cx="74" cy="56" r="7" class="tlf-17-eye fill-tlf17-eye origin-center transition-transform duration-200 group-has-[.tlf-17-email:focus-within]/card:translate-y-[3px] group-has-[.tlf-17-pw:focus-within]/card:scale-y-[0.15] motion-reduce:transition-none"></circle>
<ellipse cx="60" cy="72" rx="7" ry="5" class="fill-tlf17-nose"></ellipse>
<path d="M22 96 q10 -14 26 -8" fill="none" stroke-width="12" stroke-linecap="round" class="stroke-tlf17-fur2 origin-center transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-has-[.tlf-17-pw:focus-within]/card:[transform:rotate(-62deg)_translate(6px,-30px)] motion-reduce:transition-none"></path>
<path d="M98 96 q-10 -14 -26 -8" fill="none" stroke-width="12" stroke-linecap="round" class="stroke-tlf17-fur2 origin-center transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-has-[.tlf-17-pw:focus-within]/card:[transform:rotate(62deg)_translate(-6px,-30px)] motion-reduce:transition-none"></path>
</svg>
</div>
<h1 class="text-center text-[22px] font-bold text-tlf17-ink">Hey there!</h1>
<label class="tlf-17-email flex flex-col gap-1.5"><span class="text-[13px] font-semibold text-tlf17-label">Email</span>
<input type="email" autocomplete="email" placeholder="you@company.com" required class="h-12 px-3.5 rounded-xl text-[15px] text-tlf17-ink bg-tlf17-field border-[1.5px] border-tlf17-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf17-ph focus-visible:outline-none focus-visible:border-tlf17-acc focus-visible:ring-4 focus-visible:ring-tlf17-halo"></label>
<label class="tlf-17-pw flex flex-col gap-1.5"><span class="text-[13px] font-semibold text-tlf17-label">Password</span>
<input type="password" autocomplete="current-password" placeholder="••••••••" required class="h-12 px-3.5 rounded-xl text-[15px] text-tlf17-ink bg-tlf17-field border-[1.5px] border-tlf17-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf17-ph focus-visible:outline-none focus-visible:border-tlf17-acc focus-visible:ring-4 focus-visible:ring-tlf17-halo"></label>
<button type="submit" class="mt-1 h-12 rounded-xl border-0 cursor-pointer bg-tlf17-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-2 focus-visible:outline-tlf17-ring motion-reduce:transition-none motion-reduce:active:scale-100">Log in</button>
</form>
</section><section class="tlf-17 w-full min-h-svh grid place-items-center bg-tlf17-stage font-tlf17-sans p-6">
<form novalidate class="group/card relative w-[min(360px,100%)] flex flex-col gap-3.5 pt-16 px-8 pb-8 bg-white border border-tlf17-edge rounded-[20px] shadow-[0_24px_56px_-38px_oklch(0.4_0.08_50/0.5)]">
<div aria-hidden="true" class="absolute -top-12 left-1/2 -translate-x-1/2 size-24">
<svg viewBox="0 0 120 120" class="w-full h-full overflow-visible">
<circle cx="60" cy="60" r="44" class="fill-tlf17-fur"></circle>
<circle cx="30" cy="26" r="12" class="fill-tlf17-fur2"></circle>
<circle cx="90" cy="26" r="12" class="fill-tlf17-fur2"></circle>
<circle cx="46" cy="56" r="7" class="tlf-17-eye fill-tlf17-eye origin-center transition-transform duration-200 group-has-[.tlf-17-email:focus-within]/card:translate-y-[3px] group-has-[.tlf-17-pw:focus-within]/card:scale-y-[0.15] motion-reduce:transition-none"></circle>
<circle cx="74" cy="56" r="7" class="tlf-17-eye fill-tlf17-eye origin-center transition-transform duration-200 group-has-[.tlf-17-email:focus-within]/card:translate-y-[3px] group-has-[.tlf-17-pw:focus-within]/card:scale-y-[0.15] motion-reduce:transition-none"></circle>
<ellipse cx="60" cy="72" rx="7" ry="5" class="fill-tlf17-nose"></ellipse>
<path d="M22 96 q10 -14 26 -8" fill="none" stroke-width="12" stroke-linecap="round" class="stroke-tlf17-fur2 origin-center transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-has-[.tlf-17-pw:focus-within]/card:[transform:rotate(-62deg)_translate(6px,-30px)] motion-reduce:transition-none"></path>
<path d="M98 96 q-10 -14 -26 -8" fill="none" stroke-width="12" stroke-linecap="round" class="stroke-tlf17-fur2 origin-center transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-has-[.tlf-17-pw:focus-within]/card:[transform:rotate(62deg)_translate(-6px,-30px)] motion-reduce:transition-none"></path>
</svg>
</div>
<h1 class="text-center text-[22px] font-bold text-tlf17-ink">Hey there!</h1>
<label class="tlf-17-email flex flex-col gap-1.5"><span class="text-[13px] font-semibold text-tlf17-label">Email</span>
<input type="email" autocomplete="email" placeholder="you@company.com" required class="h-12 px-3.5 rounded-xl text-[15px] text-tlf17-ink bg-tlf17-field border-[1.5px] border-tlf17-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf17-ph focus-visible:outline-none focus-visible:border-tlf17-acc focus-visible:ring-4 focus-visible:ring-tlf17-halo"></label>
<label class="tlf-17-pw flex flex-col gap-1.5"><span class="text-[13px] font-semibold text-tlf17-label">Password</span>
<input type="password" autocomplete="current-password" placeholder="••••••••" required class="h-12 px-3.5 rounded-xl text-[15px] text-tlf17-ink bg-tlf17-field border-[1.5px] border-tlf17-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf17-ph focus-visible:outline-none focus-visible:border-tlf17-acc focus-visible:ring-4 focus-visible:ring-tlf17-halo"></label>
<button type="submit" class="mt-1 h-12 rounded-xl border-0 cursor-pointer bg-tlf17-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-2 focus-visible:outline-tlf17-ring motion-reduce:transition-none motion-reduce:active:scale-100">Log in</button>
</form>
</section>@import "tailwindcss";
@theme {
--color-tlf17-stage: #fdf3ea;
--color-tlf17-stage: oklch(0.97 0.02 65);
--color-tlf17-edge: #f0e2d5;
--color-tlf17-edge: oklch(0.93 0.02 65);
--color-tlf17-fieldedge: #ecd9c7;
--color-tlf17-fieldedge: oklch(0.9 0.03 65);
--color-tlf17-field: #fffdfb;
--color-tlf17-field: oklch(0.995 0.004 65);
--color-tlf17-ink: #4a2c15;
--color-tlf17-ink: oklch(0.33 0.06 55);
--color-tlf17-label: #6d4a2c;
--color-tlf17-label: oklch(0.44 0.06 55);
--color-tlf17-ph: #9d7e63;
--color-tlf17-ph: oklch(0.62 0.05 60);
--color-tlf17-acc: #c05a1e;
--color-tlf17-acc: oklch(0.56 0.16 45);
--color-tlf17-halo: #f0c3a4;
--color-tlf17-halo: oklch(0.56 0.16 45 / 0.18);
--color-tlf17-ring: #e39a6d;
--color-tlf17-ring: oklch(0.56 0.16 45 / 0.5);
--color-tlf17-fur: #e0a05c;
--color-tlf17-fur: oklch(0.75 0.13 65);
--color-tlf17-fur2: #cf8a45;
--color-tlf17-fur2: oklch(0.68 0.14 60);
--color-tlf17-eye: #2a1a10;
--color-tlf17-eye: oklch(0.22 0.04 50);
--color-tlf17-nose: #3a2418;
--color-tlf17-nose: oklch(0.28 0.05 50);
--font-tlf17-sans: 'Baloo 2', system-ui, sans-serif;
}
.tlf-17 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tlf17-stage: #fdf3ea;
--color-tlf17-stage: oklch(0.97 0.02 65);
--color-tlf17-edge: #f0e2d5;
--color-tlf17-edge: oklch(0.93 0.02 65);
--color-tlf17-fieldedge: #ecd9c7;
--color-tlf17-fieldedge: oklch(0.9 0.03 65);
--color-tlf17-field: #fffdfb;
--color-tlf17-field: oklch(0.995 0.004 65);
--color-tlf17-ink: #4a2c15;
--color-tlf17-ink: oklch(0.33 0.06 55);
--color-tlf17-label: #6d4a2c;
--color-tlf17-label: oklch(0.44 0.06 55);
--color-tlf17-ph: #9d7e63;
--color-tlf17-ph: oklch(0.62 0.05 60);
--color-tlf17-acc: #c05a1e;
--color-tlf17-acc: oklch(0.56 0.16 45);
--color-tlf17-halo: #f0c3a4;
--color-tlf17-halo: oklch(0.56 0.16 45 / 0.18);
--color-tlf17-ring: #e39a6d;
--color-tlf17-ring: oklch(0.56 0.16 45 / 0.5);
--color-tlf17-fur: #e0a05c;
--color-tlf17-fur: oklch(0.75 0.13 65);
--color-tlf17-fur2: #cf8a45;
--color-tlf17-fur2: oklch(0.68 0.14 60);
--color-tlf17-eye: #2a1a10;
--color-tlf17-eye: oklch(0.22 0.04 50);
--color-tlf17-nose: #3a2418;
--color-tlf17-nose: oklch(0.28 0.05 50);
--font-tlf17-sans: 'Baloo 2', system-ui, sans-serif;
}
.tlf-17 {
width: 100%;
min-height: 100svh;
}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-tlf17-stage: #fdf3ea;
--color-tlf17-stage: oklch(0.97 0.02 65);
--color-tlf17-edge: #f0e2d5;
--color-tlf17-edge: oklch(0.93 0.02 65);
--color-tlf17-fieldedge: #ecd9c7;
--color-tlf17-fieldedge: oklch(0.9 0.03 65);
--color-tlf17-field: #fffdfb;
--color-tlf17-field: oklch(0.995 0.004 65);
--color-tlf17-ink: #4a2c15;
--color-tlf17-ink: oklch(0.33 0.06 55);
--color-tlf17-label: #6d4a2c;
--color-tlf17-label: oklch(0.44 0.06 55);
--color-tlf17-ph: #9d7e63;
--color-tlf17-ph: oklch(0.62 0.05 60);
--color-tlf17-acc: #c05a1e;
--color-tlf17-acc: oklch(0.56 0.16 45);
--color-tlf17-halo: #f0c3a4;
--color-tlf17-halo: oklch(0.56 0.16 45 / 0.18);
--color-tlf17-ring: #e39a6d;
--color-tlf17-ring: oklch(0.56 0.16 45 / 0.5);
--color-tlf17-fur: #e0a05c;
--color-tlf17-fur: oklch(0.75 0.13 65);
--color-tlf17-fur2: #cf8a45;
--color-tlf17-fur2: oklch(0.68 0.14 60);
--color-tlf17-eye: #2a1a10;
--color-tlf17-eye: oklch(0.22 0.04 50);
--color-tlf17-nose: #3a2418;
--color-tlf17-nose: oklch(0.28 0.05 50);
--font-tlf17-sans: 'Baloo 2', system-ui, sans-serif;
}
.tlf-17 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tlf17-stage: #fdf3ea;
--color-tlf17-stage: oklch(0.97 0.02 65);
--color-tlf17-edge: #f0e2d5;
--color-tlf17-edge: oklch(0.93 0.02 65);
--color-tlf17-fieldedge: #ecd9c7;
--color-tlf17-fieldedge: oklch(0.9 0.03 65);
--color-tlf17-field: #fffdfb;
--color-tlf17-field: oklch(0.995 0.004 65);
--color-tlf17-ink: #4a2c15;
--color-tlf17-ink: oklch(0.33 0.06 55);
--color-tlf17-label: #6d4a2c;
--color-tlf17-label: oklch(0.44 0.06 55);
--color-tlf17-ph: #9d7e63;
--color-tlf17-ph: oklch(0.62 0.05 60);
--color-tlf17-acc: #c05a1e;
--color-tlf17-acc: oklch(0.56 0.16 45);
--color-tlf17-halo: #f0c3a4;
--color-tlf17-halo: oklch(0.56 0.16 45 / 0.18);
--color-tlf17-ring: #e39a6d;
--color-tlf17-ring: oklch(0.56 0.16 45 / 0.5);
--color-tlf17-fur: #e0a05c;
--color-tlf17-fur: oklch(0.75 0.13 65);
--color-tlf17-fur2: #cf8a45;
--color-tlf17-fur2: oklch(0.68 0.14 60);
--color-tlf17-eye: #2a1a10;
--color-tlf17-eye: oklch(0.22 0.04 50);
--color-tlf17-nose: #3a2418;
--color-tlf17-nose: oklch(0.28 0.05 50);
--font-tlf17-sans: 'Baloo 2', system-ui, sans-serif;
}
.tlf-17 {
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=Baloo+2: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=Baloo+2:wght@400;500;600;700;800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
fill-*andstroke-*utilities read@themecolours, so the character is themed from the same token block as the form — better than the original's hard-coded SVG fills, which could drift from the palette.- The two field wrappers carry marker classes (
.tlf-17-email,.tlf-17-pw) that exist purely as selector targets forgroup-has-[…]. That is a legitimate use of a bare class in a utility codebase: it is a hook, not a style. - SVG transforms as arbitrary properties rather than
rotate-*+translate-*utilities, because the two must compose in a specific order and v4's separate transform properties would apply them independently. - UI change from the CSS original: Baloo 2 replaces the system stack (a playful mascot deserves playful type), the label brown darkened from
#7a5638tooklch(0.44 0.06 55), the accent to0.56for white-text contrast, and the eyes now also react to the email field on keyboard focus, which the original's first selector attempted and mis-scoped.
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 Animated Mascot Login that Covers Its Eyes
Source: https://codefronts.com/components/tailwind-css-login-forms/tailwind-animated-mascot-login-covers-eyes/
A character sits above the form and reacts to the user: eyes glance down at the email field, then hands shoot up to cover them the moment the password field is focused. The port is group-has-*'s best demo in the collection — the CSS original's .card:has(.field--pw:focus-within) .arm becomes a variant on the arm itself.
## HTML
```html
<section class="tlf-17 w-full min-h-svh grid place-items-center bg-tlf17-stage font-tlf17-sans p-6">
<form novalidate class="group/card relative w-[min(360px,100%)] flex flex-col gap-3.5 pt-16 px-8 pb-8 bg-white border border-tlf17-edge rounded-[20px] shadow-[0_24px_56px_-38px_oklch(0.4_0.08_50/0.5)]">
<div aria-hidden="true" class="absolute -top-12 left-1/2 -translate-x-1/2 size-24">
<svg viewBox="0 0 120 120" class="w-full h-full overflow-visible">
<circle cx="60" cy="60" r="44" class="fill-tlf17-fur"></circle>
<circle cx="30" cy="26" r="12" class="fill-tlf17-fur2"></circle>
<circle cx="90" cy="26" r="12" class="fill-tlf17-fur2"></circle>
<circle cx="46" cy="56" r="7" class="tlf-17-eye fill-tlf17-eye origin-center transition-transform duration-200 group-has-[.tlf-17-email:focus-within]/card:translate-y-[3px] group-has-[.tlf-17-pw:focus-within]/card:scale-y-[0.15] motion-reduce:transition-none"></circle>
<circle cx="74" cy="56" r="7" class="tlf-17-eye fill-tlf17-eye origin-center transition-transform duration-200 group-has-[.tlf-17-email:focus-within]/card:translate-y-[3px] group-has-[.tlf-17-pw:focus-within]/card:scale-y-[0.15] motion-reduce:transition-none"></circle>
<ellipse cx="60" cy="72" rx="7" ry="5" class="fill-tlf17-nose"></ellipse>
<path d="M22 96 q10 -14 26 -8" fill="none" stroke-width="12" stroke-linecap="round" class="stroke-tlf17-fur2 origin-center transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-has-[.tlf-17-pw:focus-within]/card:[transform:rotate(-62deg)_translate(6px,-30px)] motion-reduce:transition-none"></path>
<path d="M98 96 q-10 -14 -26 -8" fill="none" stroke-width="12" stroke-linecap="round" class="stroke-tlf17-fur2 origin-center transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-has-[.tlf-17-pw:focus-within]/card:[transform:rotate(62deg)_translate(-6px,-30px)] motion-reduce:transition-none"></path>
</svg>
</div>
<h1 class="text-center text-[22px] font-bold text-tlf17-ink">Hey there!</h1>
<label class="tlf-17-email flex flex-col gap-1.5"><span class="text-[13px] font-semibold text-tlf17-label">Email</span>
<input type="email" autocomplete="email" placeholder="you@company.com" required class="h-12 px-3.5 rounded-xl text-[15px] text-tlf17-ink bg-tlf17-field border-[1.5px] border-tlf17-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf17-ph focus-visible:outline-none focus-visible:border-tlf17-acc focus-visible:ring-4 focus-visible:ring-tlf17-halo"></label>
<label class="tlf-17-pw flex flex-col gap-1.5"><span class="text-[13px] font-semibold text-tlf17-label">Password</span>
<input type="password" autocomplete="current-password" placeholder="••••••••" required class="h-12 px-3.5 rounded-xl text-[15px] text-tlf17-ink bg-tlf17-field border-[1.5px] border-tlf17-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf17-ph focus-visible:outline-none focus-visible:border-tlf17-acc focus-visible:ring-4 focus-visible:ring-tlf17-halo"></label>
<button type="submit" class="mt-1 h-12 rounded-xl border-0 cursor-pointer bg-tlf17-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-2 focus-visible:outline-tlf17-ring motion-reduce:transition-none motion-reduce:active:scale-100">Log in</button>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tlf17-stage: #fdf3ea;
--color-tlf17-stage: oklch(0.97 0.02 65);
--color-tlf17-edge: #f0e2d5;
--color-tlf17-edge: oklch(0.93 0.02 65);
--color-tlf17-fieldedge: #ecd9c7;
--color-tlf17-fieldedge: oklch(0.9 0.03 65);
--color-tlf17-field: #fffdfb;
--color-tlf17-field: oklch(0.995 0.004 65);
--color-tlf17-ink: #4a2c15;
--color-tlf17-ink: oklch(0.33 0.06 55);
--color-tlf17-label: #6d4a2c;
--color-tlf17-label: oklch(0.44 0.06 55);
--color-tlf17-ph: #9d7e63;
--color-tlf17-ph: oklch(0.62 0.05 60);
--color-tlf17-acc: #c05a1e;
--color-tlf17-acc: oklch(0.56 0.16 45);
--color-tlf17-halo: #f0c3a4;
--color-tlf17-halo: oklch(0.56 0.16 45 / 0.18);
--color-tlf17-ring: #e39a6d;
--color-tlf17-ring: oklch(0.56 0.16 45 / 0.5);
--color-tlf17-fur: #e0a05c;
--color-tlf17-fur: oklch(0.75 0.13 65);
--color-tlf17-fur2: #cf8a45;
--color-tlf17-fur2: oklch(0.68 0.14 60);
--color-tlf17-eye: #2a1a10;
--color-tlf17-eye: oklch(0.22 0.04 50);
--color-tlf17-nose: #3a2418;
--color-tlf17-nose: oklch(0.28 0.05 50);
--font-tlf17-sans: 'Baloo 2', system-ui, sans-serif;
}
.tlf-17 {
width: 100%;
min-height: 100svh;
}
```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 Animated Mascot Login that Covers Its Eyes
Source: https://codefronts.com/components/tailwind-css-login-forms/tailwind-animated-mascot-login-covers-eyes/
A character sits above the form and reacts to the user: eyes glance down at the email field, then hands shoot up to cover them the moment the password field is focused. The port is group-has-*'s best demo in the collection — the CSS original's .card:has(.field--pw:focus-within) .arm becomes a variant on the arm itself.
## HTML
```html
<section class="tlf-17 w-full min-h-svh grid place-items-center bg-tlf17-stage font-tlf17-sans p-6">
<form novalidate class="group/card relative w-[min(360px,100%)] flex flex-col gap-3.5 pt-16 px-8 pb-8 bg-white border border-tlf17-edge rounded-[20px] shadow-[0_24px_56px_-38px_oklch(0.4_0.08_50/0.5)]">
<div aria-hidden="true" class="absolute -top-12 left-1/2 -translate-x-1/2 size-24">
<svg viewBox="0 0 120 120" class="w-full h-full overflow-visible">
<circle cx="60" cy="60" r="44" class="fill-tlf17-fur"></circle>
<circle cx="30" cy="26" r="12" class="fill-tlf17-fur2"></circle>
<circle cx="90" cy="26" r="12" class="fill-tlf17-fur2"></circle>
<circle cx="46" cy="56" r="7" class="tlf-17-eye fill-tlf17-eye origin-center transition-transform duration-200 group-has-[.tlf-17-email:focus-within]/card:translate-y-[3px] group-has-[.tlf-17-pw:focus-within]/card:scale-y-[0.15] motion-reduce:transition-none"></circle>
<circle cx="74" cy="56" r="7" class="tlf-17-eye fill-tlf17-eye origin-center transition-transform duration-200 group-has-[.tlf-17-email:focus-within]/card:translate-y-[3px] group-has-[.tlf-17-pw:focus-within]/card:scale-y-[0.15] motion-reduce:transition-none"></circle>
<ellipse cx="60" cy="72" rx="7" ry="5" class="fill-tlf17-nose"></ellipse>
<path d="M22 96 q10 -14 26 -8" fill="none" stroke-width="12" stroke-linecap="round" class="stroke-tlf17-fur2 origin-center transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-has-[.tlf-17-pw:focus-within]/card:[transform:rotate(-62deg)_translate(6px,-30px)] motion-reduce:transition-none"></path>
<path d="M98 96 q-10 -14 -26 -8" fill="none" stroke-width="12" stroke-linecap="round" class="stroke-tlf17-fur2 origin-center transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-has-[.tlf-17-pw:focus-within]/card:[transform:rotate(62deg)_translate(-6px,-30px)] motion-reduce:transition-none"></path>
</svg>
</div>
<h1 class="text-center text-[22px] font-bold text-tlf17-ink">Hey there!</h1>
<label class="tlf-17-email flex flex-col gap-1.5"><span class="text-[13px] font-semibold text-tlf17-label">Email</span>
<input type="email" autocomplete="email" placeholder="you@company.com" required class="h-12 px-3.5 rounded-xl text-[15px] text-tlf17-ink bg-tlf17-field border-[1.5px] border-tlf17-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf17-ph focus-visible:outline-none focus-visible:border-tlf17-acc focus-visible:ring-4 focus-visible:ring-tlf17-halo"></label>
<label class="tlf-17-pw flex flex-col gap-1.5"><span class="text-[13px] font-semibold text-tlf17-label">Password</span>
<input type="password" autocomplete="current-password" placeholder="••••••••" required class="h-12 px-3.5 rounded-xl text-[15px] text-tlf17-ink bg-tlf17-field border-[1.5px] border-tlf17-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf17-ph focus-visible:outline-none focus-visible:border-tlf17-acc focus-visible:ring-4 focus-visible:ring-tlf17-halo"></label>
<button type="submit" class="mt-1 h-12 rounded-xl border-0 cursor-pointer bg-tlf17-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-2 focus-visible:outline-tlf17-ring motion-reduce:transition-none motion-reduce:active:scale-100">Log in</button>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tlf17-stage: #fdf3ea;
--color-tlf17-stage: oklch(0.97 0.02 65);
--color-tlf17-edge: #f0e2d5;
--color-tlf17-edge: oklch(0.93 0.02 65);
--color-tlf17-fieldedge: #ecd9c7;
--color-tlf17-fieldedge: oklch(0.9 0.03 65);
--color-tlf17-field: #fffdfb;
--color-tlf17-field: oklch(0.995 0.004 65);
--color-tlf17-ink: #4a2c15;
--color-tlf17-ink: oklch(0.33 0.06 55);
--color-tlf17-label: #6d4a2c;
--color-tlf17-label: oklch(0.44 0.06 55);
--color-tlf17-ph: #9d7e63;
--color-tlf17-ph: oklch(0.62 0.05 60);
--color-tlf17-acc: #c05a1e;
--color-tlf17-acc: oklch(0.56 0.16 45);
--color-tlf17-halo: #f0c3a4;
--color-tlf17-halo: oklch(0.56 0.16 45 / 0.18);
--color-tlf17-ring: #e39a6d;
--color-tlf17-ring: oklch(0.56 0.16 45 / 0.5);
--color-tlf17-fur: #e0a05c;
--color-tlf17-fur: oklch(0.75 0.13 65);
--color-tlf17-fur2: #cf8a45;
--color-tlf17-fur2: oklch(0.68 0.14 60);
--color-tlf17-eye: #2a1a10;
--color-tlf17-eye: oklch(0.22 0.04 50);
--color-tlf17-nose: #3a2418;
--color-tlf17-nose: oklch(0.28 0.05 50);
--font-tlf17-sans: 'Baloo 2', system-ui, sans-serif;
}
.tlf-17 {
width: 100%;
min-height: 100svh;
}
```How this works
The relation flips direction. CSS had to start from the card and reach down (:has(...) .arm). Tailwind reads it from the arm: “when my group's password field has focus within it, rotate me.” Same selector compiles out, but the behaviour is now discoverable from the element it affects, which is the entire argument of the port.
Focus, not keystrokes. :focus-within means Tab reaches the same poses a mouse click does — keyboard users get the delight, and there is no input listener to desynchronise.
The mascot is decoration. The whole SVG is aria-hidden; underneath it is an ordinary labelled form. If the character never rendered, nothing about the login would change.
Make it yours
- Arm angle and lift: the two arbitrary
[transform:…]values. - Add a typing pose with
group-has-[.tlf-17-pw:not(:placeholder-shown)]/card:. - Recolour the character in the SVG fill utilities —
fill-tlf17-fur,fill-tlf17-fur2. - Swap the art entirely; keep the
.tlf-17-eye/.tlf-17-armhooks.
Gotchas — read before shipping
- Drive it from
:focus-within, never from a JS keyup — otherwise Tab-focus leaves the mascot staring while the user types their password. - Keep the SVG
aria-hidden. A screen reader announcing "circle, circle, path" is worse than silence. group-has-[.selector]/name:needs the named group when two groups could nest; the arbitrary selector inside it must match a class that exists on the field wrapper, not on the input.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
:has() is the gate (Chrome 105, Safari 15.4, Firefox 121). Without it the mascot simply stays in its resting pose and the form is unaffected.