06 / 16
All 16 in 16 Tailwind CSS Floating Label Inputs
- 01Tailwind Staggered Letter-Cascade Floating Label
- 02Tailwind Gradient-Ink Floating Label with Animated Underline Draw
- 03Tailwind Conic Border-Beam Floating Label (@property Animation)
- 04Pure Tailwind Floating Label Input (No JavaScript)
- 05Tailwind Floating Label with Chrome Autofill Fix
- 06Tailwind Floating Label Validation States (Error + Success)
- 07Accessible Tailwind Floating Label Input (ARIA + WCAG)
- 08Tailwind Floating Label for Select Dropdowns
- 09Tailwind Material Design Outlined Text Field
- 10Tailwind Filled & Underline Floating Label Input
- 11Tailwind Dark Mode Floating Label Input with light-dark()
- 12Tailwind Input with Icon Inside and Floating Label
- 13Responsive Tailwind Floating Label for Mobile (No iOS Zoom)
- 14Tailwind Auto-Growing Textarea with Floating Label
- 15Tailwind Password Input with Show/Hide Toggle and Floating Label
- 16Tailwind Multi-Column Form Grid with Floating Labels
Tailwind Floating Label Validation States (Error + Success)
CSS-only validation that waits for the user: peer-user-invalid: turns border, label and message red only after interaction, has-[input:user-invalid]:animate-tfl06-shake nudges the field, and has-[input:user-valid]:after:opacity-100 reveals a check chip. Native constraint attributes do all the checking; the label flips on a 3D rotateX as it floats.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tfl-06 w-full min-h-svh grid place-items-center bg-tfl06-bg px-5 py-12 font-tfl06-sans" aria-label="Floating label validation demo">
<form class="flex w-full max-w-[400px] flex-col gap-2 rounded-3xl bg-white p-8 shadow-[0_24px_60px_-32px_oklch(0.3_0.03_80/0.4)] ring-1 ring-tfl06-ink/5">
<div class="mb-4 grid gap-1">
<h3 class="text-2xl font-extrabold tracking-[-0.02em] text-tfl06-ink">Create account</h3>
<p class="text-sm text-tfl06-mut">Type, then tab away to see each field validate.</p>
</div>
<div class="relative pb-[26px] has-[input:user-invalid]:animate-tfl06-shake after:pointer-events-none after:absolute after:right-3.5 after:top-7 after:grid after:size-6 after:-translate-y-1/2 after:scale-50 after:place-items-center after:rounded-full after:bg-tfl06-ok after:text-xs after:font-extrabold after:text-white after:opacity-0 after:transition-[opacity,scale] after:duration-200 after:content-['✓'] has-[input:user-valid]:after:scale-100 has-[input:user-valid]:after:opacity-100 motion-reduce:animate-none">
<input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl06-line bg-white pt-[22px] pr-12 pb-1.5 pl-4 text-base text-tfl06-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl06-line-hover focus:border-tfl06-acc focus:ring-4 focus:ring-tfl06-acc/15 user-valid:border-tfl06-ok user-invalid:border-tfl06-err user-invalid:ring-4 user-invalid:ring-tfl06-err/12" type="email" id="tfl-06-email" name="email" placeholder=" " required autocomplete="email" aria-describedby="tfl-06-email-msg">
<label class="pointer-events-none absolute left-4 top-7 origin-left -translate-y-1/2 text-base text-tfl06-mut [transform:perspective(320px)_rotateX(0deg)] transition-[translate,scale,transform,color] duration-400 ease-[cubic-bezier(.35,.7,.3,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:[transform:perspective(320px)_rotateX(360deg)] peer-focus:text-tfl06-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:[transform:perspective(320px)_rotateX(360deg)] peer-user-valid:text-tfl06-ok peer-user-invalid:text-tfl06-err motion-reduce:transition-none" for="tfl-06-email">Email address</label>
<p class="absolute bottom-1 left-0.5 text-xs/tight text-tfl06-mut transition-colors peer-user-invalid:font-semibold peer-user-invalid:text-tfl06-err" id="tfl-06-email-msg">Enter a valid email, e.g. you@domain.com</p>
</div>
<div class="relative pb-[26px] has-[input:user-invalid]:animate-tfl06-shake after:pointer-events-none after:absolute after:right-3.5 after:top-7 after:grid after:size-6 after:-translate-y-1/2 after:scale-50 after:place-items-center after:rounded-full after:bg-tfl06-ok after:text-xs after:font-extrabold after:text-white after:opacity-0 after:transition-[opacity,scale] after:duration-200 after:content-['✓'] has-[input:user-valid]:after:scale-100 has-[input:user-valid]:after:opacity-100 motion-reduce:animate-none">
<input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl06-line bg-white pt-[22px] pr-12 pb-1.5 pl-4 text-base text-tfl06-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl06-line-hover focus:border-tfl06-acc focus:ring-4 focus:ring-tfl06-acc/15 user-valid:border-tfl06-ok user-invalid:border-tfl06-err user-invalid:ring-4 user-invalid:ring-tfl06-err/12" type="text" id="tfl-06-user" name="username" placeholder=" " required minlength="4" pattern="[a-zA-Z0-9_]+" autocomplete="username" aria-describedby="tfl-06-user-msg">
<label class="pointer-events-none absolute left-4 top-7 origin-left -translate-y-1/2 text-base text-tfl06-mut [transform:perspective(320px)_rotateX(0deg)] transition-[translate,scale,transform,color] duration-400 ease-[cubic-bezier(.35,.7,.3,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:[transform:perspective(320px)_rotateX(360deg)] peer-focus:text-tfl06-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:[transform:perspective(320px)_rotateX(360deg)] peer-user-valid:text-tfl06-ok peer-user-invalid:text-tfl06-err motion-reduce:transition-none" for="tfl-06-user">Username</label>
<p class="absolute bottom-1 left-0.5 text-xs/tight text-tfl06-mut transition-colors peer-user-invalid:font-semibold peer-user-invalid:text-tfl06-err" id="tfl-06-user-msg">4+ characters — letters, numbers, underscores</p>
</div>
<button class="mt-2 min-h-12 cursor-pointer rounded-xl bg-tfl06-ink text-[15px] font-bold text-white transition-[filter,scale] duration-150 hover:brightness-150 active:scale-98 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tfl06-acc/50" type="submit">Sign up</button>
</form>
</section><section class="tfl-06 w-full min-h-svh grid place-items-center bg-tfl06-bg px-5 py-12 font-tfl06-sans" aria-label="Floating label validation demo">
<form class="flex w-full max-w-[400px] flex-col gap-2 rounded-3xl bg-white p-8 shadow-[0_24px_60px_-32px_oklch(0.3_0.03_80/0.4)] ring-1 ring-tfl06-ink/5">
<div class="mb-4 grid gap-1">
<h3 class="text-2xl font-extrabold tracking-[-0.02em] text-tfl06-ink">Create account</h3>
<p class="text-sm text-tfl06-mut">Type, then tab away to see each field validate.</p>
</div>
<div class="relative pb-[26px] has-[input:user-invalid]:animate-tfl06-shake after:pointer-events-none after:absolute after:right-3.5 after:top-7 after:grid after:size-6 after:-translate-y-1/2 after:scale-50 after:place-items-center after:rounded-full after:bg-tfl06-ok after:text-xs after:font-extrabold after:text-white after:opacity-0 after:transition-[opacity,scale] after:duration-200 after:content-['✓'] has-[input:user-valid]:after:scale-100 has-[input:user-valid]:after:opacity-100 motion-reduce:animate-none">
<input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl06-line bg-white pt-[22px] pr-12 pb-1.5 pl-4 text-base text-tfl06-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl06-line-hover focus:border-tfl06-acc focus:ring-4 focus:ring-tfl06-acc/15 user-valid:border-tfl06-ok user-invalid:border-tfl06-err user-invalid:ring-4 user-invalid:ring-tfl06-err/12" type="email" id="tfl-06-email" name="email" placeholder=" " required autocomplete="email" aria-describedby="tfl-06-email-msg">
<label class="pointer-events-none absolute left-4 top-7 origin-left -translate-y-1/2 text-base text-tfl06-mut [transform:perspective(320px)_rotateX(0deg)] transition-[translate,scale,transform,color] duration-400 ease-[cubic-bezier(.35,.7,.3,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:[transform:perspective(320px)_rotateX(360deg)] peer-focus:text-tfl06-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:[transform:perspective(320px)_rotateX(360deg)] peer-user-valid:text-tfl06-ok peer-user-invalid:text-tfl06-err motion-reduce:transition-none" for="tfl-06-email">Email address</label>
<p class="absolute bottom-1 left-0.5 text-xs/tight text-tfl06-mut transition-colors peer-user-invalid:font-semibold peer-user-invalid:text-tfl06-err" id="tfl-06-email-msg">Enter a valid email, e.g. you@domain.com</p>
</div>
<div class="relative pb-[26px] has-[input:user-invalid]:animate-tfl06-shake after:pointer-events-none after:absolute after:right-3.5 after:top-7 after:grid after:size-6 after:-translate-y-1/2 after:scale-50 after:place-items-center after:rounded-full after:bg-tfl06-ok after:text-xs after:font-extrabold after:text-white after:opacity-0 after:transition-[opacity,scale] after:duration-200 after:content-['✓'] has-[input:user-valid]:after:scale-100 has-[input:user-valid]:after:opacity-100 motion-reduce:animate-none">
<input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl06-line bg-white pt-[22px] pr-12 pb-1.5 pl-4 text-base text-tfl06-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl06-line-hover focus:border-tfl06-acc focus:ring-4 focus:ring-tfl06-acc/15 user-valid:border-tfl06-ok user-invalid:border-tfl06-err user-invalid:ring-4 user-invalid:ring-tfl06-err/12" type="text" id="tfl-06-user" name="username" placeholder=" " required minlength="4" pattern="[a-zA-Z0-9_]+" autocomplete="username" aria-describedby="tfl-06-user-msg">
<label class="pointer-events-none absolute left-4 top-7 origin-left -translate-y-1/2 text-base text-tfl06-mut [transform:perspective(320px)_rotateX(0deg)] transition-[translate,scale,transform,color] duration-400 ease-[cubic-bezier(.35,.7,.3,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:[transform:perspective(320px)_rotateX(360deg)] peer-focus:text-tfl06-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:[transform:perspective(320px)_rotateX(360deg)] peer-user-valid:text-tfl06-ok peer-user-invalid:text-tfl06-err motion-reduce:transition-none" for="tfl-06-user">Username</label>
<p class="absolute bottom-1 left-0.5 text-xs/tight text-tfl06-mut transition-colors peer-user-invalid:font-semibold peer-user-invalid:text-tfl06-err" id="tfl-06-user-msg">4+ characters — letters, numbers, underscores</p>
</div>
<button class="mt-2 min-h-12 cursor-pointer rounded-xl bg-tfl06-ink text-[15px] font-bold text-white transition-[filter,scale] duration-150 hover:brightness-150 active:scale-98 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tfl06-acc/50" type="submit">Sign up</button>
</form>
</section>@import "tailwindcss";
@theme {
--color-tfl06-bg: #f4f3f0;
--color-tfl06-bg: oklch(0.96 0.005 90);
--color-tfl06-ink: #221f18;
--color-tfl06-ink: oklch(0.23 0.012 85);
--color-tfl06-mut: #6a6558;
--color-tfl06-mut: oklch(0.5 0.02 85);
--color-tfl06-line: #d9d6cd;
--color-tfl06-line: oklch(0.87 0.012 90);
--color-tfl06-line-hover: #bdb8ab;
--color-tfl06-line-hover: oklch(0.78 0.018 90);
--color-tfl06-acc: #4a50d4;
--color-tfl06-acc: oklch(0.5 0.2 272);
--color-tfl06-err: #c8372e;
--color-tfl06-err: oklch(0.55 0.19 27);
--color-tfl06-ok: #1f8a4c;
--color-tfl06-ok: oklch(0.55 0.14 152);
--font-tfl06-sans: 'DM Sans', system-ui, sans-serif;
--animate-tfl06-shake: tfl06-shake 0.3s ease;
@keyframes tfl06-shake {
20% {
transform: translateX(-5px);
}
40% {
transform: translateX(5px);
}
60% {
transform: translateX(-3px);
}
80% {
transform: translateX(3px);
}
}
}
.tfl-06 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tfl06-bg: #f4f3f0;
--color-tfl06-bg: oklch(0.96 0.005 90);
--color-tfl06-ink: #221f18;
--color-tfl06-ink: oklch(0.23 0.012 85);
--color-tfl06-mut: #6a6558;
--color-tfl06-mut: oklch(0.5 0.02 85);
--color-tfl06-line: #d9d6cd;
--color-tfl06-line: oklch(0.87 0.012 90);
--color-tfl06-line-hover: #bdb8ab;
--color-tfl06-line-hover: oklch(0.78 0.018 90);
--color-tfl06-acc: #4a50d4;
--color-tfl06-acc: oklch(0.5 0.2 272);
--color-tfl06-err: #c8372e;
--color-tfl06-err: oklch(0.55 0.19 27);
--color-tfl06-ok: #1f8a4c;
--color-tfl06-ok: oklch(0.55 0.14 152);
--font-tfl06-sans: 'DM Sans', system-ui, sans-serif;
--animate-tfl06-shake: tfl06-shake 0.3s ease;
@keyframes tfl06-shake {
20% {
transform: translateX(-5px);
}
40% {
transform: translateX(5px);
}
60% {
transform: translateX(-3px);
}
80% {
transform: translateX(3px);
}
}
}
.tfl-06 {
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-tfl06-bg: #f4f3f0;
--color-tfl06-bg: oklch(0.96 0.005 90);
--color-tfl06-ink: #221f18;
--color-tfl06-ink: oklch(0.23 0.012 85);
--color-tfl06-mut: #6a6558;
--color-tfl06-mut: oklch(0.5 0.02 85);
--color-tfl06-line: #d9d6cd;
--color-tfl06-line: oklch(0.87 0.012 90);
--color-tfl06-line-hover: #bdb8ab;
--color-tfl06-line-hover: oklch(0.78 0.018 90);
--color-tfl06-acc: #4a50d4;
--color-tfl06-acc: oklch(0.5 0.2 272);
--color-tfl06-err: #c8372e;
--color-tfl06-err: oklch(0.55 0.19 27);
--color-tfl06-ok: #1f8a4c;
--color-tfl06-ok: oklch(0.55 0.14 152);
--font-tfl06-sans: 'DM Sans', system-ui, sans-serif;
--animate-tfl06-shake: tfl06-shake 0.3s ease;
@keyframes tfl06-shake {
20% {
transform: translateX(-5px);
}
40% {
transform: translateX(5px);
}
60% {
transform: translateX(-3px);
}
80% {
transform: translateX(3px);
}
}
}
.tfl-06 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tfl06-bg: #f4f3f0;
--color-tfl06-bg: oklch(0.96 0.005 90);
--color-tfl06-ink: #221f18;
--color-tfl06-ink: oklch(0.23 0.012 85);
--color-tfl06-mut: #6a6558;
--color-tfl06-mut: oklch(0.5 0.02 85);
--color-tfl06-line: #d9d6cd;
--color-tfl06-line: oklch(0.87 0.012 90);
--color-tfl06-line-hover: #bdb8ab;
--color-tfl06-line-hover: oklch(0.78 0.018 90);
--color-tfl06-acc: #4a50d4;
--color-tfl06-acc: oklch(0.5 0.2 272);
--color-tfl06-err: #c8372e;
--color-tfl06-err: oklch(0.55 0.19 27);
--color-tfl06-ok: #1f8a4c;
--color-tfl06-ok: oklch(0.55 0.14 152);
--font-tfl06-sans: 'DM Sans', system-ui, sans-serif;
--animate-tfl06-shake: tfl06-shake 0.3s ease;
@keyframes tfl06-shake {
20% {
transform: translateX(-5px);
}
40% {
transform: translateX(5px);
}
60% {
transform: translateX(-3px);
}
80% {
transform: translateX(3px);
}
}
}
.tfl-06 {
width: 100%;
min-height: 100svh;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400..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=DM+Sans:opsz,wght@9..40,400..800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Six state rules became
user-invalid:/user-valid:prefixes on the input, andpeer-user-*on the label and message — each element lists every state it can be in. .field:has(input:user-invalid)becamehas-[input:user-invalid]:animate-tfl06-shake; the keyframes moved into@theme.- UI upgrade from the CSS original: the success mark is now a filled chip that scales in, not a bare 17px glyph; helper text rose from
#9a9588(2.9:1, failing) tooklch(0.5)(5.6:1). - UI upgrade: the success green deepened to 4.6:1 so the floated green label passes AA, and a one-line instruction tells users how validation is triggered.
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 Floating Label Validation States (Error + Success)
Source: https://codefronts.com/components/tailwind-css-floating-label-inputs/tailwind-floating-label-validation-states/
CSS-only validation that waits for the user: peer-user-invalid: turns border, label and message red only after interaction, has-[input:user-invalid]:animate-tfl06-shake nudges the field, and has-[input:user-valid]:after:opacity-100 reveals a check chip. Native constraint attributes do all the checking; the label flips on a 3D rotateX as it floats.
## HTML
```html
<section class="tfl-06 w-full min-h-svh grid place-items-center bg-tfl06-bg px-5 py-12 font-tfl06-sans" aria-label="Floating label validation demo">
<form class="flex w-full max-w-[400px] flex-col gap-2 rounded-3xl bg-white p-8 shadow-[0_24px_60px_-32px_oklch(0.3_0.03_80/0.4)] ring-1 ring-tfl06-ink/5">
<div class="mb-4 grid gap-1">
<h3 class="text-2xl font-extrabold tracking-[-0.02em] text-tfl06-ink">Create account</h3>
<p class="text-sm text-tfl06-mut">Type, then tab away to see each field validate.</p>
</div>
<div class="relative pb-[26px] has-[input:user-invalid]:animate-tfl06-shake after:pointer-events-none after:absolute after:right-3.5 after:top-7 after:grid after:size-6 after:-translate-y-1/2 after:scale-50 after:place-items-center after:rounded-full after:bg-tfl06-ok after:text-xs after:font-extrabold after:text-white after:opacity-0 after:transition-[opacity,scale] after:duration-200 after:content-['✓'] has-[input:user-valid]:after:scale-100 has-[input:user-valid]:after:opacity-100 motion-reduce:animate-none">
<input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl06-line bg-white pt-[22px] pr-12 pb-1.5 pl-4 text-base text-tfl06-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl06-line-hover focus:border-tfl06-acc focus:ring-4 focus:ring-tfl06-acc/15 user-valid:border-tfl06-ok user-invalid:border-tfl06-err user-invalid:ring-4 user-invalid:ring-tfl06-err/12" type="email" id="tfl-06-email" name="email" placeholder=" " required autocomplete="email" aria-describedby="tfl-06-email-msg">
<label class="pointer-events-none absolute left-4 top-7 origin-left -translate-y-1/2 text-base text-tfl06-mut [transform:perspective(320px)_rotateX(0deg)] transition-[translate,scale,transform,color] duration-400 ease-[cubic-bezier(.35,.7,.3,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:[transform:perspective(320px)_rotateX(360deg)] peer-focus:text-tfl06-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:[transform:perspective(320px)_rotateX(360deg)] peer-user-valid:text-tfl06-ok peer-user-invalid:text-tfl06-err motion-reduce:transition-none" for="tfl-06-email">Email address</label>
<p class="absolute bottom-1 left-0.5 text-xs/tight text-tfl06-mut transition-colors peer-user-invalid:font-semibold peer-user-invalid:text-tfl06-err" id="tfl-06-email-msg">Enter a valid email, e.g. you@domain.com</p>
</div>
<div class="relative pb-[26px] has-[input:user-invalid]:animate-tfl06-shake after:pointer-events-none after:absolute after:right-3.5 after:top-7 after:grid after:size-6 after:-translate-y-1/2 after:scale-50 after:place-items-center after:rounded-full after:bg-tfl06-ok after:text-xs after:font-extrabold after:text-white after:opacity-0 after:transition-[opacity,scale] after:duration-200 after:content-['✓'] has-[input:user-valid]:after:scale-100 has-[input:user-valid]:after:opacity-100 motion-reduce:animate-none">
<input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl06-line bg-white pt-[22px] pr-12 pb-1.5 pl-4 text-base text-tfl06-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl06-line-hover focus:border-tfl06-acc focus:ring-4 focus:ring-tfl06-acc/15 user-valid:border-tfl06-ok user-invalid:border-tfl06-err user-invalid:ring-4 user-invalid:ring-tfl06-err/12" type="text" id="tfl-06-user" name="username" placeholder=" " required minlength="4" pattern="[a-zA-Z0-9_]+" autocomplete="username" aria-describedby="tfl-06-user-msg">
<label class="pointer-events-none absolute left-4 top-7 origin-left -translate-y-1/2 text-base text-tfl06-mut [transform:perspective(320px)_rotateX(0deg)] transition-[translate,scale,transform,color] duration-400 ease-[cubic-bezier(.35,.7,.3,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:[transform:perspective(320px)_rotateX(360deg)] peer-focus:text-tfl06-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:[transform:perspective(320px)_rotateX(360deg)] peer-user-valid:text-tfl06-ok peer-user-invalid:text-tfl06-err motion-reduce:transition-none" for="tfl-06-user">Username</label>
<p class="absolute bottom-1 left-0.5 text-xs/tight text-tfl06-mut transition-colors peer-user-invalid:font-semibold peer-user-invalid:text-tfl06-err" id="tfl-06-user-msg">4+ characters — letters, numbers, underscores</p>
</div>
<button class="mt-2 min-h-12 cursor-pointer rounded-xl bg-tfl06-ink text-[15px] font-bold text-white transition-[filter,scale] duration-150 hover:brightness-150 active:scale-98 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tfl06-acc/50" type="submit">Sign up</button>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tfl06-bg: #f4f3f0;
--color-tfl06-bg: oklch(0.96 0.005 90);
--color-tfl06-ink: #221f18;
--color-tfl06-ink: oklch(0.23 0.012 85);
--color-tfl06-mut: #6a6558;
--color-tfl06-mut: oklch(0.5 0.02 85);
--color-tfl06-line: #d9d6cd;
--color-tfl06-line: oklch(0.87 0.012 90);
--color-tfl06-line-hover: #bdb8ab;
--color-tfl06-line-hover: oklch(0.78 0.018 90);
--color-tfl06-acc: #4a50d4;
--color-tfl06-acc: oklch(0.5 0.2 272);
--color-tfl06-err: #c8372e;
--color-tfl06-err: oklch(0.55 0.19 27);
--color-tfl06-ok: #1f8a4c;
--color-tfl06-ok: oklch(0.55 0.14 152);
--font-tfl06-sans: 'DM Sans', system-ui, sans-serif;
--animate-tfl06-shake: tfl06-shake 0.3s ease;
@keyframes tfl06-shake {
20% {
transform: translateX(-5px);
}
40% {
transform: translateX(5px);
}
60% {
transform: translateX(-3px);
}
80% {
transform: translateX(3px);
}
}
}
.tfl-06 {
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 Floating Label Validation States (Error + Success)
Source: https://codefronts.com/components/tailwind-css-floating-label-inputs/tailwind-floating-label-validation-states/
CSS-only validation that waits for the user: peer-user-invalid: turns border, label and message red only after interaction, has-[input:user-invalid]:animate-tfl06-shake nudges the field, and has-[input:user-valid]:after:opacity-100 reveals a check chip. Native constraint attributes do all the checking; the label flips on a 3D rotateX as it floats.
## HTML
```html
<section class="tfl-06 w-full min-h-svh grid place-items-center bg-tfl06-bg px-5 py-12 font-tfl06-sans" aria-label="Floating label validation demo">
<form class="flex w-full max-w-[400px] flex-col gap-2 rounded-3xl bg-white p-8 shadow-[0_24px_60px_-32px_oklch(0.3_0.03_80/0.4)] ring-1 ring-tfl06-ink/5">
<div class="mb-4 grid gap-1">
<h3 class="text-2xl font-extrabold tracking-[-0.02em] text-tfl06-ink">Create account</h3>
<p class="text-sm text-tfl06-mut">Type, then tab away to see each field validate.</p>
</div>
<div class="relative pb-[26px] has-[input:user-invalid]:animate-tfl06-shake after:pointer-events-none after:absolute after:right-3.5 after:top-7 after:grid after:size-6 after:-translate-y-1/2 after:scale-50 after:place-items-center after:rounded-full after:bg-tfl06-ok after:text-xs after:font-extrabold after:text-white after:opacity-0 after:transition-[opacity,scale] after:duration-200 after:content-['✓'] has-[input:user-valid]:after:scale-100 has-[input:user-valid]:after:opacity-100 motion-reduce:animate-none">
<input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl06-line bg-white pt-[22px] pr-12 pb-1.5 pl-4 text-base text-tfl06-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl06-line-hover focus:border-tfl06-acc focus:ring-4 focus:ring-tfl06-acc/15 user-valid:border-tfl06-ok user-invalid:border-tfl06-err user-invalid:ring-4 user-invalid:ring-tfl06-err/12" type="email" id="tfl-06-email" name="email" placeholder=" " required autocomplete="email" aria-describedby="tfl-06-email-msg">
<label class="pointer-events-none absolute left-4 top-7 origin-left -translate-y-1/2 text-base text-tfl06-mut [transform:perspective(320px)_rotateX(0deg)] transition-[translate,scale,transform,color] duration-400 ease-[cubic-bezier(.35,.7,.3,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:[transform:perspective(320px)_rotateX(360deg)] peer-focus:text-tfl06-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:[transform:perspective(320px)_rotateX(360deg)] peer-user-valid:text-tfl06-ok peer-user-invalid:text-tfl06-err motion-reduce:transition-none" for="tfl-06-email">Email address</label>
<p class="absolute bottom-1 left-0.5 text-xs/tight text-tfl06-mut transition-colors peer-user-invalid:font-semibold peer-user-invalid:text-tfl06-err" id="tfl-06-email-msg">Enter a valid email, e.g. you@domain.com</p>
</div>
<div class="relative pb-[26px] has-[input:user-invalid]:animate-tfl06-shake after:pointer-events-none after:absolute after:right-3.5 after:top-7 after:grid after:size-6 after:-translate-y-1/2 after:scale-50 after:place-items-center after:rounded-full after:bg-tfl06-ok after:text-xs after:font-extrabold after:text-white after:opacity-0 after:transition-[opacity,scale] after:duration-200 after:content-['✓'] has-[input:user-valid]:after:scale-100 has-[input:user-valid]:after:opacity-100 motion-reduce:animate-none">
<input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl06-line bg-white pt-[22px] pr-12 pb-1.5 pl-4 text-base text-tfl06-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl06-line-hover focus:border-tfl06-acc focus:ring-4 focus:ring-tfl06-acc/15 user-valid:border-tfl06-ok user-invalid:border-tfl06-err user-invalid:ring-4 user-invalid:ring-tfl06-err/12" type="text" id="tfl-06-user" name="username" placeholder=" " required minlength="4" pattern="[a-zA-Z0-9_]+" autocomplete="username" aria-describedby="tfl-06-user-msg">
<label class="pointer-events-none absolute left-4 top-7 origin-left -translate-y-1/2 text-base text-tfl06-mut [transform:perspective(320px)_rotateX(0deg)] transition-[translate,scale,transform,color] duration-400 ease-[cubic-bezier(.35,.7,.3,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:[transform:perspective(320px)_rotateX(360deg)] peer-focus:text-tfl06-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:[transform:perspective(320px)_rotateX(360deg)] peer-user-valid:text-tfl06-ok peer-user-invalid:text-tfl06-err motion-reduce:transition-none" for="tfl-06-user">Username</label>
<p class="absolute bottom-1 left-0.5 text-xs/tight text-tfl06-mut transition-colors peer-user-invalid:font-semibold peer-user-invalid:text-tfl06-err" id="tfl-06-user-msg">4+ characters — letters, numbers, underscores</p>
</div>
<button class="mt-2 min-h-12 cursor-pointer rounded-xl bg-tfl06-ink text-[15px] font-bold text-white transition-[filter,scale] duration-150 hover:brightness-150 active:scale-98 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tfl06-acc/50" type="submit">Sign up</button>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tfl06-bg: #f4f3f0;
--color-tfl06-bg: oklch(0.96 0.005 90);
--color-tfl06-ink: #221f18;
--color-tfl06-ink: oklch(0.23 0.012 85);
--color-tfl06-mut: #6a6558;
--color-tfl06-mut: oklch(0.5 0.02 85);
--color-tfl06-line: #d9d6cd;
--color-tfl06-line: oklch(0.87 0.012 90);
--color-tfl06-line-hover: #bdb8ab;
--color-tfl06-line-hover: oklch(0.78 0.018 90);
--color-tfl06-acc: #4a50d4;
--color-tfl06-acc: oklch(0.5 0.2 272);
--color-tfl06-err: #c8372e;
--color-tfl06-err: oklch(0.55 0.19 27);
--color-tfl06-ok: #1f8a4c;
--color-tfl06-ok: oklch(0.55 0.14 152);
--font-tfl06-sans: 'DM Sans', system-ui, sans-serif;
--animate-tfl06-shake: tfl06-shake 0.3s ease;
@keyframes tfl06-shake {
20% {
transform: translateX(-5px);
}
40% {
transform: translateX(5px);
}
60% {
transform: translateX(-3px);
}
80% {
transform: translateX(3px);
}
}
}
.tfl-06 {
width: 100%;
min-height: 100svh;
}
```How this works
user-invalid, not invalid. Tailwind's invalid: marks a required empty field red on page load; user-invalid: waits until the user has typed and left. Each state is one prefix on the element that changes:
input: user-invalid:border-tfl06-err user-valid:border-tfl06-ok
label: peer-user-invalid:text-tfl06-err
msg: peer-user-invalid:text-tfl06-errThe flip needs a matching resting transform. Transitions only interpolate between compatible function lists, so the resting label carries perspective(320px) rotateX(0deg) and the float carries rotateX(360deg); translate and scale stay on their own properties.
Make it yours
- Palettes:
--color-tfl06-errand--color-tfl06-ok. - Shake: edit
@keyframes tfl06-shakeinside@theme. - Rules: change
pattern/minlength— no class changes needed. - Calm enterprise forms: delete the
has-[…]:animate-tfl06-shakeclass, keep the colours.
Gotchas — read before shipping
- Keep error text in the DOM and referenced by
aria-describedby, so it is read with the field. - The shake animates
transformonly and is cancelled bymotion-reduce:animate-none. - With
novalidateremoved, the browser's own bubble also appears on submit — that is intentional here.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 119+ (:user-invalid) | 16.5+ | 128+ | 119+ (:user-invalid) |
Where :user-invalid is unsupported, fields stay neutral and native validation still blocks submit.