02 / 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 Gradient-Ink Floating Label with Animated Underline Draw
The floated label fills with flowing gradient ink while a matching underline draws itself beneath the field. The gradient lives once, as an arbitrary custom property on the root ([--tfl02-ink:linear-gradient(…)]), and every surface reads it with bg-[image:var(--tfl02-ink)]. bg-clip-text plus a [-webkit-text-fill-color:…] flip reveals it.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tfl-02 w-full min-h-svh grid place-items-center bg-tfl02-bg bg-[radial-gradient(70%_60%_at_50%_0%,var(--color-tfl02-glow),transparent_70%)] px-5 py-12 font-tfl02-sans [--tfl02-ink:linear-gradient(90deg,var(--color-tfl02-a),var(--color-tfl02-b),var(--color-tfl02-c),var(--color-tfl02-a))]" aria-label="Gradient ink floating label demo">
<form class="flex w-full max-w-[390px] flex-col gap-6 rounded-3xl border border-tfl02-edge bg-tfl02-card p-8 shadow-[0_30px_80px_-40px_oklch(0_0_0/0.8)]" novalidate>
<div class="grid gap-1.5">
<p class="w-fit bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] bg-clip-text text-xs font-bold uppercase tracking-[0.14em] text-transparent">Private beta</p>
<h3 class="text-2xl font-bold tracking-[-0.02em] text-tfl02-ink">Reserve your invite</h3>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-[2.5px] after:origin-left after:scale-x-0 after:rounded-full after:bg-[image:var(--tfl02-ink)] after:bg-[length:220%_100%] after:transition-[scale] after:duration-400 after:delay-120 after:ease-[cubic-bezier(.25,.8,.3,1)] after:content-[''] focus-within:after:scale-x-100 focus-within:after:animate-tfl02-flow has-[input:not(:placeholder-shown)]:after:scale-x-100 has-[input:not(:placeholder-shown)]:after:animate-tfl02-flow motion-reduce:after:animate-none">
<input class="peer h-14 w-full rounded-t-xl rounded-b-sm border-[1.5px] border-tfl02-line bg-tfl02-field px-4 pt-[22px] pb-1.5 text-base text-tfl02-ink outline-hidden transition-[border-color] duration-200 focus:border-tfl02-line-focus" type="text" id="tfl-02-handle" name="nickname" placeholder=" " autocomplete="nickname">
<label class="pointer-events-none absolute left-4 top-1/2 origin-left -translate-y-1/2 bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] bg-clip-text text-base [-webkit-text-fill-color:var(--color-tfl02-mut)] transition-[translate,scale,-webkit-text-fill-color] duration-[250ms] ease-[cubic-bezier(.3,1.2,.4,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-74 peer-focus:font-bold peer-focus:[-webkit-text-fill-color:transparent] peer-focus:animate-tfl02-flow peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-74 peer-[:not(:placeholder-shown)]:font-bold peer-[:not(:placeholder-shown)]:[-webkit-text-fill-color:transparent] peer-[:not(:placeholder-shown)]:animate-tfl02-flow motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-02-handle">Pick a handle</label>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-[2.5px] after:origin-left after:scale-x-0 after:rounded-full after:bg-[image:var(--tfl02-ink)] after:bg-[length:220%_100%] after:transition-[scale] after:duration-400 after:delay-120 after:ease-[cubic-bezier(.25,.8,.3,1)] after:content-[''] focus-within:after:scale-x-100 focus-within:after:animate-tfl02-flow has-[input:not(:placeholder-shown)]:after:scale-x-100 has-[input:not(:placeholder-shown)]:after:animate-tfl02-flow motion-reduce:after:animate-none">
<input class="peer h-14 w-full rounded-t-xl rounded-b-sm border-[1.5px] border-tfl02-line bg-tfl02-field px-4 pt-[22px] pb-1.5 text-base text-tfl02-ink outline-hidden transition-[border-color] duration-200 focus:border-tfl02-line-focus" type="email" id="tfl-02-mail" name="email" placeholder=" " autocomplete="email">
<label class="pointer-events-none absolute left-4 top-1/2 origin-left -translate-y-1/2 bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] bg-clip-text text-base [-webkit-text-fill-color:var(--color-tfl02-mut)] transition-[translate,scale,-webkit-text-fill-color] duration-[250ms] ease-[cubic-bezier(.3,1.2,.4,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-74 peer-focus:font-bold peer-focus:[-webkit-text-fill-color:transparent] peer-focus:animate-tfl02-flow peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-74 peer-[:not(:placeholder-shown)]:font-bold peer-[:not(:placeholder-shown)]:[-webkit-text-fill-color:transparent] peer-[:not(:placeholder-shown)]:animate-tfl02-flow motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-02-mail">Email address</label>
</div>
<button class="min-h-12 cursor-pointer rounded-xl bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] text-[15px] font-extrabold text-white [text-shadow:0_1px_2px_oklch(0_0_0/0.35)] transition-[filter,scale] duration-150 hover:animate-tfl02-flow hover:brightness-110 active:scale-98 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tfl02-b motion-reduce:hover:animate-none" type="button">Join the waitlist</button>
</form>
</section><section class="tfl-02 w-full min-h-svh grid place-items-center bg-tfl02-bg bg-[radial-gradient(70%_60%_at_50%_0%,var(--color-tfl02-glow),transparent_70%)] px-5 py-12 font-tfl02-sans [--tfl02-ink:linear-gradient(90deg,var(--color-tfl02-a),var(--color-tfl02-b),var(--color-tfl02-c),var(--color-tfl02-a))]" aria-label="Gradient ink floating label demo">
<form class="flex w-full max-w-[390px] flex-col gap-6 rounded-3xl border border-tfl02-edge bg-tfl02-card p-8 shadow-[0_30px_80px_-40px_oklch(0_0_0/0.8)]" novalidate>
<div class="grid gap-1.5">
<p class="w-fit bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] bg-clip-text text-xs font-bold uppercase tracking-[0.14em] text-transparent">Private beta</p>
<h3 class="text-2xl font-bold tracking-[-0.02em] text-tfl02-ink">Reserve your invite</h3>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-[2.5px] after:origin-left after:scale-x-0 after:rounded-full after:bg-[image:var(--tfl02-ink)] after:bg-[length:220%_100%] after:transition-[scale] after:duration-400 after:delay-120 after:ease-[cubic-bezier(.25,.8,.3,1)] after:content-[''] focus-within:after:scale-x-100 focus-within:after:animate-tfl02-flow has-[input:not(:placeholder-shown)]:after:scale-x-100 has-[input:not(:placeholder-shown)]:after:animate-tfl02-flow motion-reduce:after:animate-none">
<input class="peer h-14 w-full rounded-t-xl rounded-b-sm border-[1.5px] border-tfl02-line bg-tfl02-field px-4 pt-[22px] pb-1.5 text-base text-tfl02-ink outline-hidden transition-[border-color] duration-200 focus:border-tfl02-line-focus" type="text" id="tfl-02-handle" name="nickname" placeholder=" " autocomplete="nickname">
<label class="pointer-events-none absolute left-4 top-1/2 origin-left -translate-y-1/2 bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] bg-clip-text text-base [-webkit-text-fill-color:var(--color-tfl02-mut)] transition-[translate,scale,-webkit-text-fill-color] duration-[250ms] ease-[cubic-bezier(.3,1.2,.4,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-74 peer-focus:font-bold peer-focus:[-webkit-text-fill-color:transparent] peer-focus:animate-tfl02-flow peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-74 peer-[:not(:placeholder-shown)]:font-bold peer-[:not(:placeholder-shown)]:[-webkit-text-fill-color:transparent] peer-[:not(:placeholder-shown)]:animate-tfl02-flow motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-02-handle">Pick a handle</label>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-[2.5px] after:origin-left after:scale-x-0 after:rounded-full after:bg-[image:var(--tfl02-ink)] after:bg-[length:220%_100%] after:transition-[scale] after:duration-400 after:delay-120 after:ease-[cubic-bezier(.25,.8,.3,1)] after:content-[''] focus-within:after:scale-x-100 focus-within:after:animate-tfl02-flow has-[input:not(:placeholder-shown)]:after:scale-x-100 has-[input:not(:placeholder-shown)]:after:animate-tfl02-flow motion-reduce:after:animate-none">
<input class="peer h-14 w-full rounded-t-xl rounded-b-sm border-[1.5px] border-tfl02-line bg-tfl02-field px-4 pt-[22px] pb-1.5 text-base text-tfl02-ink outline-hidden transition-[border-color] duration-200 focus:border-tfl02-line-focus" type="email" id="tfl-02-mail" name="email" placeholder=" " autocomplete="email">
<label class="pointer-events-none absolute left-4 top-1/2 origin-left -translate-y-1/2 bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] bg-clip-text text-base [-webkit-text-fill-color:var(--color-tfl02-mut)] transition-[translate,scale,-webkit-text-fill-color] duration-[250ms] ease-[cubic-bezier(.3,1.2,.4,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-74 peer-focus:font-bold peer-focus:[-webkit-text-fill-color:transparent] peer-focus:animate-tfl02-flow peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-74 peer-[:not(:placeholder-shown)]:font-bold peer-[:not(:placeholder-shown)]:[-webkit-text-fill-color:transparent] peer-[:not(:placeholder-shown)]:animate-tfl02-flow motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-02-mail">Email address</label>
</div>
<button class="min-h-12 cursor-pointer rounded-xl bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] text-[15px] font-extrabold text-white [text-shadow:0_1px_2px_oklch(0_0_0/0.35)] transition-[filter,scale] duration-150 hover:animate-tfl02-flow hover:brightness-110 active:scale-98 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tfl02-b motion-reduce:hover:animate-none" type="button">Join the waitlist</button>
</form>
</section>@import "tailwindcss";
@theme {
--color-tfl02-bg: #141118;
--color-tfl02-bg: oklch(0.18 0.015 300);
--color-tfl02-glow: #2a1f3a;
--color-tfl02-glow: oklch(0.27 0.05 300);
--color-tfl02-card: #1d1924;
--color-tfl02-card: oklch(0.22 0.018 300);
--color-tfl02-edge: #2e2839;
--color-tfl02-edge: oklch(0.29 0.025 300);
--color-tfl02-field: #161320;
--color-tfl02-field: oklch(0.19 0.02 295);
--color-tfl02-line: #332c42;
--color-tfl02-line: oklch(0.31 0.035 300);
--color-tfl02-line-focus: #4a4060;
--color-tfl02-line-focus: oklch(0.39 0.05 300);
--color-tfl02-ink: #f2eef7;
--color-tfl02-ink: oklch(0.95 0.01 300);
--color-tfl02-mut: #a39bb5;
--color-tfl02-mut: oklch(0.7 0.03 300);
--color-tfl02-a: #a06cf0;
--color-tfl02-a: oklch(0.65 0.18 300);
--color-tfl02-b: #3ea5e8;
--color-tfl02-b: oklch(0.7 0.14 235);
--color-tfl02-c: #f07a82;
--color-tfl02-c: oklch(0.72 0.14 15);
--font-tfl02-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tfl02-flow: tfl02-flow 4s linear infinite;
@keyframes tfl02-flow {
to {
background-position: -220% 0;
}
}
}
.tfl-02 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tfl02-bg: #141118;
--color-tfl02-bg: oklch(0.18 0.015 300);
--color-tfl02-glow: #2a1f3a;
--color-tfl02-glow: oklch(0.27 0.05 300);
--color-tfl02-card: #1d1924;
--color-tfl02-card: oklch(0.22 0.018 300);
--color-tfl02-edge: #2e2839;
--color-tfl02-edge: oklch(0.29 0.025 300);
--color-tfl02-field: #161320;
--color-tfl02-field: oklch(0.19 0.02 295);
--color-tfl02-line: #332c42;
--color-tfl02-line: oklch(0.31 0.035 300);
--color-tfl02-line-focus: #4a4060;
--color-tfl02-line-focus: oklch(0.39 0.05 300);
--color-tfl02-ink: #f2eef7;
--color-tfl02-ink: oklch(0.95 0.01 300);
--color-tfl02-mut: #a39bb5;
--color-tfl02-mut: oklch(0.7 0.03 300);
--color-tfl02-a: #a06cf0;
--color-tfl02-a: oklch(0.65 0.18 300);
--color-tfl02-b: #3ea5e8;
--color-tfl02-b: oklch(0.7 0.14 235);
--color-tfl02-c: #f07a82;
--color-tfl02-c: oklch(0.72 0.14 15);
--font-tfl02-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tfl02-flow: tfl02-flow 4s linear infinite;
@keyframes tfl02-flow {
to {
background-position: -220% 0;
}
}
}
.tfl-02 {
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-tfl02-bg: #141118;
--color-tfl02-bg: oklch(0.18 0.015 300);
--color-tfl02-glow: #2a1f3a;
--color-tfl02-glow: oklch(0.27 0.05 300);
--color-tfl02-card: #1d1924;
--color-tfl02-card: oklch(0.22 0.018 300);
--color-tfl02-edge: #2e2839;
--color-tfl02-edge: oklch(0.29 0.025 300);
--color-tfl02-field: #161320;
--color-tfl02-field: oklch(0.19 0.02 295);
--color-tfl02-line: #332c42;
--color-tfl02-line: oklch(0.31 0.035 300);
--color-tfl02-line-focus: #4a4060;
--color-tfl02-line-focus: oklch(0.39 0.05 300);
--color-tfl02-ink: #f2eef7;
--color-tfl02-ink: oklch(0.95 0.01 300);
--color-tfl02-mut: #a39bb5;
--color-tfl02-mut: oklch(0.7 0.03 300);
--color-tfl02-a: #a06cf0;
--color-tfl02-a: oklch(0.65 0.18 300);
--color-tfl02-b: #3ea5e8;
--color-tfl02-b: oklch(0.7 0.14 235);
--color-tfl02-c: #f07a82;
--color-tfl02-c: oklch(0.72 0.14 15);
--font-tfl02-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tfl02-flow: tfl02-flow 4s linear infinite;
@keyframes tfl02-flow {
to {
background-position: -220% 0;
}
}
}
.tfl-02 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tfl02-bg: #141118;
--color-tfl02-bg: oklch(0.18 0.015 300);
--color-tfl02-glow: #2a1f3a;
--color-tfl02-glow: oklch(0.27 0.05 300);
--color-tfl02-card: #1d1924;
--color-tfl02-card: oklch(0.22 0.018 300);
--color-tfl02-edge: #2e2839;
--color-tfl02-edge: oklch(0.29 0.025 300);
--color-tfl02-field: #161320;
--color-tfl02-field: oklch(0.19 0.02 295);
--color-tfl02-line: #332c42;
--color-tfl02-line: oklch(0.31 0.035 300);
--color-tfl02-line-focus: #4a4060;
--color-tfl02-line-focus: oklch(0.39 0.05 300);
--color-tfl02-ink: #f2eef7;
--color-tfl02-ink: oklch(0.95 0.01 300);
--color-tfl02-mut: #a39bb5;
--color-tfl02-mut: oklch(0.7 0.03 300);
--color-tfl02-a: #a06cf0;
--color-tfl02-a: oklch(0.65 0.18 300);
--color-tfl02-b: #3ea5e8;
--color-tfl02-b: oklch(0.7 0.14 235);
--color-tfl02-c: #f07a82;
--color-tfl02-c: oklch(0.72 0.14 15);
--font-tfl02-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tfl02-flow: tfl02-flow 4s linear infinite;
@keyframes tfl02-flow {
to {
background-position: -220% 0;
}
}
}
.tfl-02 {
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=Space+Grotesk:wght@400..700&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=Space+Grotesk:wght@400..700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The
--inkcustom property moved from a CSS rule into an arbitrary property on the root element — the same single source of truth, now visible in the markup. .field:has(input:not(:placeholder-shown))::afterbecamehas-[input:not(:placeholder-shown)]:after:scale-x-100; the CSS original's dead.flb-02__noperule was dropped.- UI upgrade from the CSS original: the gradient stops were lightened (they sat near 3.5:1 on the dark field at floated size) and the resting label rose from
#8d84a0tooklch(0.7), 6.5:1. - UI upgrade: the button pans its gradient on hover, gains a text shadow for legibility on the lighter stops, and a gradient-ink eyebrow ties the header into the effect.
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 Gradient-Ink Floating Label with Animated Underline Draw
Source: https://codefronts.com/components/tailwind-css-floating-label-inputs/tailwind-gradient-ink-floating-label-underline-draw/
The floated label fills with flowing gradient ink while a matching underline draws itself beneath the field. The gradient lives once, as an arbitrary custom property on the root ([--tfl02-ink:linear-gradient(…)]), and every surface reads it with bg-[image:var(--tfl02-ink)]. bg-clip-text plus a [-webkit-text-fill-color:…] flip reveals it.
## HTML
```html
<section class="tfl-02 w-full min-h-svh grid place-items-center bg-tfl02-bg bg-[radial-gradient(70%_60%_at_50%_0%,var(--color-tfl02-glow),transparent_70%)] px-5 py-12 font-tfl02-sans [--tfl02-ink:linear-gradient(90deg,var(--color-tfl02-a),var(--color-tfl02-b),var(--color-tfl02-c),var(--color-tfl02-a))]" aria-label="Gradient ink floating label demo">
<form class="flex w-full max-w-[390px] flex-col gap-6 rounded-3xl border border-tfl02-edge bg-tfl02-card p-8 shadow-[0_30px_80px_-40px_oklch(0_0_0/0.8)]" novalidate>
<div class="grid gap-1.5">
<p class="w-fit bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] bg-clip-text text-xs font-bold uppercase tracking-[0.14em] text-transparent">Private beta</p>
<h3 class="text-2xl font-bold tracking-[-0.02em] text-tfl02-ink">Reserve your invite</h3>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-[2.5px] after:origin-left after:scale-x-0 after:rounded-full after:bg-[image:var(--tfl02-ink)] after:bg-[length:220%_100%] after:transition-[scale] after:duration-400 after:delay-120 after:ease-[cubic-bezier(.25,.8,.3,1)] after:content-[''] focus-within:after:scale-x-100 focus-within:after:animate-tfl02-flow has-[input:not(:placeholder-shown)]:after:scale-x-100 has-[input:not(:placeholder-shown)]:after:animate-tfl02-flow motion-reduce:after:animate-none">
<input class="peer h-14 w-full rounded-t-xl rounded-b-sm border-[1.5px] border-tfl02-line bg-tfl02-field px-4 pt-[22px] pb-1.5 text-base text-tfl02-ink outline-hidden transition-[border-color] duration-200 focus:border-tfl02-line-focus" type="text" id="tfl-02-handle" name="nickname" placeholder=" " autocomplete="nickname">
<label class="pointer-events-none absolute left-4 top-1/2 origin-left -translate-y-1/2 bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] bg-clip-text text-base [-webkit-text-fill-color:var(--color-tfl02-mut)] transition-[translate,scale,-webkit-text-fill-color] duration-[250ms] ease-[cubic-bezier(.3,1.2,.4,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-74 peer-focus:font-bold peer-focus:[-webkit-text-fill-color:transparent] peer-focus:animate-tfl02-flow peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-74 peer-[:not(:placeholder-shown)]:font-bold peer-[:not(:placeholder-shown)]:[-webkit-text-fill-color:transparent] peer-[:not(:placeholder-shown)]:animate-tfl02-flow motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-02-handle">Pick a handle</label>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-[2.5px] after:origin-left after:scale-x-0 after:rounded-full after:bg-[image:var(--tfl02-ink)] after:bg-[length:220%_100%] after:transition-[scale] after:duration-400 after:delay-120 after:ease-[cubic-bezier(.25,.8,.3,1)] after:content-[''] focus-within:after:scale-x-100 focus-within:after:animate-tfl02-flow has-[input:not(:placeholder-shown)]:after:scale-x-100 has-[input:not(:placeholder-shown)]:after:animate-tfl02-flow motion-reduce:after:animate-none">
<input class="peer h-14 w-full rounded-t-xl rounded-b-sm border-[1.5px] border-tfl02-line bg-tfl02-field px-4 pt-[22px] pb-1.5 text-base text-tfl02-ink outline-hidden transition-[border-color] duration-200 focus:border-tfl02-line-focus" type="email" id="tfl-02-mail" name="email" placeholder=" " autocomplete="email">
<label class="pointer-events-none absolute left-4 top-1/2 origin-left -translate-y-1/2 bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] bg-clip-text text-base [-webkit-text-fill-color:var(--color-tfl02-mut)] transition-[translate,scale,-webkit-text-fill-color] duration-[250ms] ease-[cubic-bezier(.3,1.2,.4,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-74 peer-focus:font-bold peer-focus:[-webkit-text-fill-color:transparent] peer-focus:animate-tfl02-flow peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-74 peer-[:not(:placeholder-shown)]:font-bold peer-[:not(:placeholder-shown)]:[-webkit-text-fill-color:transparent] peer-[:not(:placeholder-shown)]:animate-tfl02-flow motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-02-mail">Email address</label>
</div>
<button class="min-h-12 cursor-pointer rounded-xl bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] text-[15px] font-extrabold text-white [text-shadow:0_1px_2px_oklch(0_0_0/0.35)] transition-[filter,scale] duration-150 hover:animate-tfl02-flow hover:brightness-110 active:scale-98 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tfl02-b motion-reduce:hover:animate-none" type="button">Join the waitlist</button>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tfl02-bg: #141118;
--color-tfl02-bg: oklch(0.18 0.015 300);
--color-tfl02-glow: #2a1f3a;
--color-tfl02-glow: oklch(0.27 0.05 300);
--color-tfl02-card: #1d1924;
--color-tfl02-card: oklch(0.22 0.018 300);
--color-tfl02-edge: #2e2839;
--color-tfl02-edge: oklch(0.29 0.025 300);
--color-tfl02-field: #161320;
--color-tfl02-field: oklch(0.19 0.02 295);
--color-tfl02-line: #332c42;
--color-tfl02-line: oklch(0.31 0.035 300);
--color-tfl02-line-focus: #4a4060;
--color-tfl02-line-focus: oklch(0.39 0.05 300);
--color-tfl02-ink: #f2eef7;
--color-tfl02-ink: oklch(0.95 0.01 300);
--color-tfl02-mut: #a39bb5;
--color-tfl02-mut: oklch(0.7 0.03 300);
--color-tfl02-a: #a06cf0;
--color-tfl02-a: oklch(0.65 0.18 300);
--color-tfl02-b: #3ea5e8;
--color-tfl02-b: oklch(0.7 0.14 235);
--color-tfl02-c: #f07a82;
--color-tfl02-c: oklch(0.72 0.14 15);
--font-tfl02-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tfl02-flow: tfl02-flow 4s linear infinite;
@keyframes tfl02-flow {
to {
background-position: -220% 0;
}
}
}
.tfl-02 {
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 Gradient-Ink Floating Label with Animated Underline Draw
Source: https://codefronts.com/components/tailwind-css-floating-label-inputs/tailwind-gradient-ink-floating-label-underline-draw/
The floated label fills with flowing gradient ink while a matching underline draws itself beneath the field. The gradient lives once, as an arbitrary custom property on the root ([--tfl02-ink:linear-gradient(…)]), and every surface reads it with bg-[image:var(--tfl02-ink)]. bg-clip-text plus a [-webkit-text-fill-color:…] flip reveals it.
## HTML
```html
<section class="tfl-02 w-full min-h-svh grid place-items-center bg-tfl02-bg bg-[radial-gradient(70%_60%_at_50%_0%,var(--color-tfl02-glow),transparent_70%)] px-5 py-12 font-tfl02-sans [--tfl02-ink:linear-gradient(90deg,var(--color-tfl02-a),var(--color-tfl02-b),var(--color-tfl02-c),var(--color-tfl02-a))]" aria-label="Gradient ink floating label demo">
<form class="flex w-full max-w-[390px] flex-col gap-6 rounded-3xl border border-tfl02-edge bg-tfl02-card p-8 shadow-[0_30px_80px_-40px_oklch(0_0_0/0.8)]" novalidate>
<div class="grid gap-1.5">
<p class="w-fit bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] bg-clip-text text-xs font-bold uppercase tracking-[0.14em] text-transparent">Private beta</p>
<h3 class="text-2xl font-bold tracking-[-0.02em] text-tfl02-ink">Reserve your invite</h3>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-[2.5px] after:origin-left after:scale-x-0 after:rounded-full after:bg-[image:var(--tfl02-ink)] after:bg-[length:220%_100%] after:transition-[scale] after:duration-400 after:delay-120 after:ease-[cubic-bezier(.25,.8,.3,1)] after:content-[''] focus-within:after:scale-x-100 focus-within:after:animate-tfl02-flow has-[input:not(:placeholder-shown)]:after:scale-x-100 has-[input:not(:placeholder-shown)]:after:animate-tfl02-flow motion-reduce:after:animate-none">
<input class="peer h-14 w-full rounded-t-xl rounded-b-sm border-[1.5px] border-tfl02-line bg-tfl02-field px-4 pt-[22px] pb-1.5 text-base text-tfl02-ink outline-hidden transition-[border-color] duration-200 focus:border-tfl02-line-focus" type="text" id="tfl-02-handle" name="nickname" placeholder=" " autocomplete="nickname">
<label class="pointer-events-none absolute left-4 top-1/2 origin-left -translate-y-1/2 bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] bg-clip-text text-base [-webkit-text-fill-color:var(--color-tfl02-mut)] transition-[translate,scale,-webkit-text-fill-color] duration-[250ms] ease-[cubic-bezier(.3,1.2,.4,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-74 peer-focus:font-bold peer-focus:[-webkit-text-fill-color:transparent] peer-focus:animate-tfl02-flow peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-74 peer-[:not(:placeholder-shown)]:font-bold peer-[:not(:placeholder-shown)]:[-webkit-text-fill-color:transparent] peer-[:not(:placeholder-shown)]:animate-tfl02-flow motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-02-handle">Pick a handle</label>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-[2.5px] after:origin-left after:scale-x-0 after:rounded-full after:bg-[image:var(--tfl02-ink)] after:bg-[length:220%_100%] after:transition-[scale] after:duration-400 after:delay-120 after:ease-[cubic-bezier(.25,.8,.3,1)] after:content-[''] focus-within:after:scale-x-100 focus-within:after:animate-tfl02-flow has-[input:not(:placeholder-shown)]:after:scale-x-100 has-[input:not(:placeholder-shown)]:after:animate-tfl02-flow motion-reduce:after:animate-none">
<input class="peer h-14 w-full rounded-t-xl rounded-b-sm border-[1.5px] border-tfl02-line bg-tfl02-field px-4 pt-[22px] pb-1.5 text-base text-tfl02-ink outline-hidden transition-[border-color] duration-200 focus:border-tfl02-line-focus" type="email" id="tfl-02-mail" name="email" placeholder=" " autocomplete="email">
<label class="pointer-events-none absolute left-4 top-1/2 origin-left -translate-y-1/2 bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] bg-clip-text text-base [-webkit-text-fill-color:var(--color-tfl02-mut)] transition-[translate,scale,-webkit-text-fill-color] duration-[250ms] ease-[cubic-bezier(.3,1.2,.4,1)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-74 peer-focus:font-bold peer-focus:[-webkit-text-fill-color:transparent] peer-focus:animate-tfl02-flow peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-74 peer-[:not(:placeholder-shown)]:font-bold peer-[:not(:placeholder-shown)]:[-webkit-text-fill-color:transparent] peer-[:not(:placeholder-shown)]:animate-tfl02-flow motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-02-mail">Email address</label>
</div>
<button class="min-h-12 cursor-pointer rounded-xl bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] text-[15px] font-extrabold text-white [text-shadow:0_1px_2px_oklch(0_0_0/0.35)] transition-[filter,scale] duration-150 hover:animate-tfl02-flow hover:brightness-110 active:scale-98 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tfl02-b motion-reduce:hover:animate-none" type="button">Join the waitlist</button>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tfl02-bg: #141118;
--color-tfl02-bg: oklch(0.18 0.015 300);
--color-tfl02-glow: #2a1f3a;
--color-tfl02-glow: oklch(0.27 0.05 300);
--color-tfl02-card: #1d1924;
--color-tfl02-card: oklch(0.22 0.018 300);
--color-tfl02-edge: #2e2839;
--color-tfl02-edge: oklch(0.29 0.025 300);
--color-tfl02-field: #161320;
--color-tfl02-field: oklch(0.19 0.02 295);
--color-tfl02-line: #332c42;
--color-tfl02-line: oklch(0.31 0.035 300);
--color-tfl02-line-focus: #4a4060;
--color-tfl02-line-focus: oklch(0.39 0.05 300);
--color-tfl02-ink: #f2eef7;
--color-tfl02-ink: oklch(0.95 0.01 300);
--color-tfl02-mut: #a39bb5;
--color-tfl02-mut: oklch(0.7 0.03 300);
--color-tfl02-a: #a06cf0;
--color-tfl02-a: oklch(0.65 0.18 300);
--color-tfl02-b: #3ea5e8;
--color-tfl02-b: oklch(0.7 0.14 235);
--color-tfl02-c: #f07a82;
--color-tfl02-c: oklch(0.72 0.14 15);
--font-tfl02-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tfl02-flow: tfl02-flow 4s linear infinite;
@keyframes tfl02-flow {
to {
background-position: -220% 0;
}
}
}
.tfl-02 {
width: 100%;
min-height: 100svh;
}
```How this works
One gradient, three consumers. Instead of repeating a four-stop gradient in three arbitrary values, the root declares it once as a custom property and each consumer reads it:
<section class="[--tfl02-ink:linear-gradient(90deg,var(--color-tfl02-a),…)]">
<label class="bg-[image:var(--tfl02-ink)] bg-[length:220%_100%] bg-clip-text …">The reveal is a fill-colour flip. color has no effect on clipped text; the resting label paints [-webkit-text-fill-color:var(--color-tfl02-mut)] over the gradient, and the float swaps it to transparent.
The underline is the field's after: — after:content-[''] is required, since Tailwind only emits the pseudo-element when content is set.
Make it yours
- Swap the three stop tokens
--color-tfl02-a/b/c; the first is repeated last so the pan loops seamlessly. - Flow speed: the duration in
--animate-tfl02-flow. - Static gradient: drop the
animate-tfl02-flowclasses, keep the reveal. - Underline timing:
after:delay-120andafter:duration-400.
Gotchas — read before shipping
- Toggle
-webkit-text-fill-color, notcolor— colour is invisible on clipped text. - Every stop must stay ≥4.5:1 on the field at floated size.
bg-[image:…]needs theimage:type hint — a barevar()is read as a colour.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
bg-clip-text emits the -webkit- prefix for you. Reduced motion keeps the gradient and stops the pan.