10 / 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 Filled & Underline Floating Label Input
The calm, editorial filled field: a tinted box with only a bottom border, where focus sweeps an accent underline from the centre outward (after:scale-x-0 focus-within:after:scale-x-100) and the label performs a blink-swap — fading out mid-field and re-materialising in its floated spot via animate-tfl10-swap.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tfl-10 w-full min-h-svh grid place-items-center bg-tfl10-bg px-5 py-12 font-tfl10-sans" aria-label="Filled underline floating label demo">
<form class="flex w-full max-w-[380px] flex-col gap-[22px] rounded-[18px] bg-white p-8 shadow-[0_20px_50px_-30px_oklch(0.3_0.04_210/0.35)] ring-1 ring-tfl10-ink/5" novalidate>
<div class="grid gap-1">
<h3 class="font-tfl10-serif text-[30px]/tight text-tfl10-ink">Stay in the <em class="text-tfl10-acc">loop</em></h3>
<p class="text-sm text-tfl10-mut">One thoughtful email a month. No noise.</p>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:origin-center after:scale-x-0 after:bg-tfl10-acc after:transition-[scale] after:duration-[250ms] after:ease-out after:content-[''] focus-within:after:scale-x-100 motion-reduce:after:transition-none">
<input class="peer h-14 w-full rounded-t-[10px] border-b-2 border-tfl10-line bg-tfl10-acc/7 px-3.5 pt-[22px] pb-1.5 text-base text-tfl10-ink outline-hidden transition-[background-color] duration-200 hover:bg-tfl10-acc/11 focus:bg-tfl10-acc/10" type="text" id="tfl-10-first" name="given-name" placeholder=" " autocomplete="given-name">
<label class="pointer-events-none absolute left-3.5 top-1/2 origin-left -translate-y-1/2 text-base text-tfl10-mut transition-[translate,scale,color] duration-[180ms] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:animate-tfl10-swap peer-focus:text-tfl10-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:animate-tfl10-swap motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-10-first">First name</label>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:origin-center after:scale-x-0 after:bg-tfl10-acc after:transition-[scale] after:duration-[250ms] after:ease-out after:content-[''] focus-within:after:scale-x-100 motion-reduce:after:transition-none">
<input class="peer h-14 w-full rounded-t-[10px] border-b-2 border-tfl10-line bg-tfl10-acc/7 px-3.5 pt-[22px] pb-1.5 text-base text-tfl10-ink outline-hidden transition-[background-color] duration-200 hover:bg-tfl10-acc/11 focus:bg-tfl10-acc/10" type="email" id="tfl-10-mail" name="email" placeholder=" " autocomplete="email">
<label class="pointer-events-none absolute left-3.5 top-1/2 origin-left -translate-y-1/2 text-base text-tfl10-mut transition-[translate,scale,color] duration-[180ms] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:animate-tfl10-swap peer-focus:text-tfl10-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:animate-tfl10-swap motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-10-mail">Email address</label>
</div>
<button class="min-h-12 cursor-pointer rounded-xl bg-tfl10-ink text-[15px] font-semibold 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-tfl10-acc/60" type="button">Subscribe</button>
</form>
</section><section class="tfl-10 w-full min-h-svh grid place-items-center bg-tfl10-bg px-5 py-12 font-tfl10-sans" aria-label="Filled underline floating label demo">
<form class="flex w-full max-w-[380px] flex-col gap-[22px] rounded-[18px] bg-white p-8 shadow-[0_20px_50px_-30px_oklch(0.3_0.04_210/0.35)] ring-1 ring-tfl10-ink/5" novalidate>
<div class="grid gap-1">
<h3 class="font-tfl10-serif text-[30px]/tight text-tfl10-ink">Stay in the <em class="text-tfl10-acc">loop</em></h3>
<p class="text-sm text-tfl10-mut">One thoughtful email a month. No noise.</p>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:origin-center after:scale-x-0 after:bg-tfl10-acc after:transition-[scale] after:duration-[250ms] after:ease-out after:content-[''] focus-within:after:scale-x-100 motion-reduce:after:transition-none">
<input class="peer h-14 w-full rounded-t-[10px] border-b-2 border-tfl10-line bg-tfl10-acc/7 px-3.5 pt-[22px] pb-1.5 text-base text-tfl10-ink outline-hidden transition-[background-color] duration-200 hover:bg-tfl10-acc/11 focus:bg-tfl10-acc/10" type="text" id="tfl-10-first" name="given-name" placeholder=" " autocomplete="given-name">
<label class="pointer-events-none absolute left-3.5 top-1/2 origin-left -translate-y-1/2 text-base text-tfl10-mut transition-[translate,scale,color] duration-[180ms] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:animate-tfl10-swap peer-focus:text-tfl10-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:animate-tfl10-swap motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-10-first">First name</label>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:origin-center after:scale-x-0 after:bg-tfl10-acc after:transition-[scale] after:duration-[250ms] after:ease-out after:content-[''] focus-within:after:scale-x-100 motion-reduce:after:transition-none">
<input class="peer h-14 w-full rounded-t-[10px] border-b-2 border-tfl10-line bg-tfl10-acc/7 px-3.5 pt-[22px] pb-1.5 text-base text-tfl10-ink outline-hidden transition-[background-color] duration-200 hover:bg-tfl10-acc/11 focus:bg-tfl10-acc/10" type="email" id="tfl-10-mail" name="email" placeholder=" " autocomplete="email">
<label class="pointer-events-none absolute left-3.5 top-1/2 origin-left -translate-y-1/2 text-base text-tfl10-mut transition-[translate,scale,color] duration-[180ms] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:animate-tfl10-swap peer-focus:text-tfl10-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:animate-tfl10-swap motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-10-mail">Email address</label>
</div>
<button class="min-h-12 cursor-pointer rounded-xl bg-tfl10-ink text-[15px] font-semibold 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-tfl10-acc/60" type="button">Subscribe</button>
</form>
</section>@import "tailwindcss";
@theme {
--color-tfl10-bg: #f3f6f6;
--color-tfl10-bg: oklch(0.97 0.004 200);
--color-tfl10-ink: #16232a;
--color-tfl10-ink: oklch(0.24 0.02 225);
--color-tfl10-mut: #56696f;
--color-tfl10-mut: oklch(0.49 0.025 215);
--color-tfl10-line: #a9b8bf;
--color-tfl10-line: oklch(0.77 0.02 220);
--color-tfl10-acc: #0f7f98;
--color-tfl10-acc: oklch(0.53 0.1 215);
--font-tfl10-sans: 'Instrument Sans', system-ui, sans-serif;
--font-tfl10-serif: 'Instrument Serif', Georgia, serif;
--animate-tfl10-swap: tfl10-swap 0.32s both;
@keyframes tfl10-swap {
0% {
opacity: 1;
translate: 0 -50%;
scale: 1;
}
45% {
opacity: 0;
translate: 0 calc(-50% + 5px);
scale: 1;
}
55% {
opacity: 0;
translate: 0 calc(-50% - 18px);
scale: 0.72;
}
100% {
opacity: 1;
translate: 0 calc(-50% - 13px);
scale: 0.72;
}
}
}
.tfl-10 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tfl10-bg: #f3f6f6;
--color-tfl10-bg: oklch(0.97 0.004 200);
--color-tfl10-ink: #16232a;
--color-tfl10-ink: oklch(0.24 0.02 225);
--color-tfl10-mut: #56696f;
--color-tfl10-mut: oklch(0.49 0.025 215);
--color-tfl10-line: #a9b8bf;
--color-tfl10-line: oklch(0.77 0.02 220);
--color-tfl10-acc: #0f7f98;
--color-tfl10-acc: oklch(0.53 0.1 215);
--font-tfl10-sans: 'Instrument Sans', system-ui, sans-serif;
--font-tfl10-serif: 'Instrument Serif', Georgia, serif;
--animate-tfl10-swap: tfl10-swap 0.32s both;
@keyframes tfl10-swap {
0% {
opacity: 1;
translate: 0 -50%;
scale: 1;
}
45% {
opacity: 0;
translate: 0 calc(-50% + 5px);
scale: 1;
}
55% {
opacity: 0;
translate: 0 calc(-50% - 18px);
scale: 0.72;
}
100% {
opacity: 1;
translate: 0 calc(-50% - 13px);
scale: 0.72;
}
}
}
.tfl-10 {
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-tfl10-bg: #f3f6f6;
--color-tfl10-bg: oklch(0.97 0.004 200);
--color-tfl10-ink: #16232a;
--color-tfl10-ink: oklch(0.24 0.02 225);
--color-tfl10-mut: #56696f;
--color-tfl10-mut: oklch(0.49 0.025 215);
--color-tfl10-line: #a9b8bf;
--color-tfl10-line: oklch(0.77 0.02 220);
--color-tfl10-acc: #0f7f98;
--color-tfl10-acc: oklch(0.53 0.1 215);
--font-tfl10-sans: 'Instrument Sans', system-ui, sans-serif;
--font-tfl10-serif: 'Instrument Serif', Georgia, serif;
--animate-tfl10-swap: tfl10-swap 0.32s both;
@keyframes tfl10-swap {
0% {
opacity: 1;
translate: 0 -50%;
scale: 1;
}
45% {
opacity: 0;
translate: 0 calc(-50% + 5px);
scale: 1;
}
55% {
opacity: 0;
translate: 0 calc(-50% - 18px);
scale: 0.72;
}
100% {
opacity: 1;
translate: 0 calc(-50% - 13px);
scale: 0.72;
}
}
}
.tfl-10 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tfl10-bg: #f3f6f6;
--color-tfl10-bg: oklch(0.97 0.004 200);
--color-tfl10-ink: #16232a;
--color-tfl10-ink: oklch(0.24 0.02 225);
--color-tfl10-mut: #56696f;
--color-tfl10-mut: oklch(0.49 0.025 215);
--color-tfl10-line: #a9b8bf;
--color-tfl10-line: oklch(0.77 0.02 220);
--color-tfl10-acc: #0f7f98;
--color-tfl10-acc: oklch(0.53 0.1 215);
--font-tfl10-sans: 'Instrument Sans', system-ui, sans-serif;
--font-tfl10-serif: 'Instrument Serif', Georgia, serif;
--animate-tfl10-swap: tfl10-swap 0.32s both;
@keyframes tfl10-swap {
0% {
opacity: 1;
translate: 0 -50%;
scale: 1;
}
45% {
opacity: 0;
translate: 0 calc(-50% + 5px);
scale: 1;
}
55% {
opacity: 0;
translate: 0 calc(-50% - 18px);
scale: 0.72;
}
100% {
opacity: 1;
translate: 0 calc(-50% - 13px);
scale: 0.72;
}
}
}
.tfl-10 {
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=Instrument+Sans:wght@400..700&family=Instrument+Serif:ital@0;1&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=Instrument+Sans:wght@400..700&family=Instrument+Serif:ital@0;1&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The tint
oklch(… / .07)became the opacity modifierbg-tfl10-acc/7, so hover and focus tints are one character each. - The swap keyframes were rewritten on
translate/scaleinstead oftransformto match how v4 floats — the original mixed the two, so the end frame and the transition target disagreed by the-50%centring. - UI upgrade from the CSS original: a focus tint joins the underline so the active field is obvious even before the sweep completes; the button's focus outline now uses the accent at AA strength.
- UI upgrade: Instrument Serif headline with an italic accent word, a real sub-line, and the resting bottom border darkened from
#b9c6ccto meet 3:1 non-text contrast.
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 Filled & Underline Floating Label Input
Source: https://codefronts.com/components/tailwind-css-floating-label-inputs/tailwind-filled-underline-floating-label-input/
The calm, editorial filled field: a tinted box with only a bottom border, where focus sweeps an accent underline from the centre outward (after:scale-x-0 focus-within:after:scale-x-100) and the label performs a blink-swap — fading out mid-field and re-materialising in its floated spot via animate-tfl10-swap.
## HTML
```html
<section class="tfl-10 w-full min-h-svh grid place-items-center bg-tfl10-bg px-5 py-12 font-tfl10-sans" aria-label="Filled underline floating label demo">
<form class="flex w-full max-w-[380px] flex-col gap-[22px] rounded-[18px] bg-white p-8 shadow-[0_20px_50px_-30px_oklch(0.3_0.04_210/0.35)] ring-1 ring-tfl10-ink/5" novalidate>
<div class="grid gap-1">
<h3 class="font-tfl10-serif text-[30px]/tight text-tfl10-ink">Stay in the <em class="text-tfl10-acc">loop</em></h3>
<p class="text-sm text-tfl10-mut">One thoughtful email a month. No noise.</p>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:origin-center after:scale-x-0 after:bg-tfl10-acc after:transition-[scale] after:duration-[250ms] after:ease-out after:content-[''] focus-within:after:scale-x-100 motion-reduce:after:transition-none">
<input class="peer h-14 w-full rounded-t-[10px] border-b-2 border-tfl10-line bg-tfl10-acc/7 px-3.5 pt-[22px] pb-1.5 text-base text-tfl10-ink outline-hidden transition-[background-color] duration-200 hover:bg-tfl10-acc/11 focus:bg-tfl10-acc/10" type="text" id="tfl-10-first" name="given-name" placeholder=" " autocomplete="given-name">
<label class="pointer-events-none absolute left-3.5 top-1/2 origin-left -translate-y-1/2 text-base text-tfl10-mut transition-[translate,scale,color] duration-[180ms] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:animate-tfl10-swap peer-focus:text-tfl10-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:animate-tfl10-swap motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-10-first">First name</label>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:origin-center after:scale-x-0 after:bg-tfl10-acc after:transition-[scale] after:duration-[250ms] after:ease-out after:content-[''] focus-within:after:scale-x-100 motion-reduce:after:transition-none">
<input class="peer h-14 w-full rounded-t-[10px] border-b-2 border-tfl10-line bg-tfl10-acc/7 px-3.5 pt-[22px] pb-1.5 text-base text-tfl10-ink outline-hidden transition-[background-color] duration-200 hover:bg-tfl10-acc/11 focus:bg-tfl10-acc/10" type="email" id="tfl-10-mail" name="email" placeholder=" " autocomplete="email">
<label class="pointer-events-none absolute left-3.5 top-1/2 origin-left -translate-y-1/2 text-base text-tfl10-mut transition-[translate,scale,color] duration-[180ms] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:animate-tfl10-swap peer-focus:text-tfl10-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:animate-tfl10-swap motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-10-mail">Email address</label>
</div>
<button class="min-h-12 cursor-pointer rounded-xl bg-tfl10-ink text-[15px] font-semibold 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-tfl10-acc/60" type="button">Subscribe</button>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tfl10-bg: #f3f6f6;
--color-tfl10-bg: oklch(0.97 0.004 200);
--color-tfl10-ink: #16232a;
--color-tfl10-ink: oklch(0.24 0.02 225);
--color-tfl10-mut: #56696f;
--color-tfl10-mut: oklch(0.49 0.025 215);
--color-tfl10-line: #a9b8bf;
--color-tfl10-line: oklch(0.77 0.02 220);
--color-tfl10-acc: #0f7f98;
--color-tfl10-acc: oklch(0.53 0.1 215);
--font-tfl10-sans: 'Instrument Sans', system-ui, sans-serif;
--font-tfl10-serif: 'Instrument Serif', Georgia, serif;
--animate-tfl10-swap: tfl10-swap 0.32s both;
@keyframes tfl10-swap {
0% {
opacity: 1;
translate: 0 -50%;
scale: 1;
}
45% {
opacity: 0;
translate: 0 calc(-50% + 5px);
scale: 1;
}
55% {
opacity: 0;
translate: 0 calc(-50% - 18px);
scale: 0.72;
}
100% {
opacity: 1;
translate: 0 calc(-50% - 13px);
scale: 0.72;
}
}
}
.tfl-10 {
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 Filled & Underline Floating Label Input
Source: https://codefronts.com/components/tailwind-css-floating-label-inputs/tailwind-filled-underline-floating-label-input/
The calm, editorial filled field: a tinted box with only a bottom border, where focus sweeps an accent underline from the centre outward (after:scale-x-0 focus-within:after:scale-x-100) and the label performs a blink-swap — fading out mid-field and re-materialising in its floated spot via animate-tfl10-swap.
## HTML
```html
<section class="tfl-10 w-full min-h-svh grid place-items-center bg-tfl10-bg px-5 py-12 font-tfl10-sans" aria-label="Filled underline floating label demo">
<form class="flex w-full max-w-[380px] flex-col gap-[22px] rounded-[18px] bg-white p-8 shadow-[0_20px_50px_-30px_oklch(0.3_0.04_210/0.35)] ring-1 ring-tfl10-ink/5" novalidate>
<div class="grid gap-1">
<h3 class="font-tfl10-serif text-[30px]/tight text-tfl10-ink">Stay in the <em class="text-tfl10-acc">loop</em></h3>
<p class="text-sm text-tfl10-mut">One thoughtful email a month. No noise.</p>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:origin-center after:scale-x-0 after:bg-tfl10-acc after:transition-[scale] after:duration-[250ms] after:ease-out after:content-[''] focus-within:after:scale-x-100 motion-reduce:after:transition-none">
<input class="peer h-14 w-full rounded-t-[10px] border-b-2 border-tfl10-line bg-tfl10-acc/7 px-3.5 pt-[22px] pb-1.5 text-base text-tfl10-ink outline-hidden transition-[background-color] duration-200 hover:bg-tfl10-acc/11 focus:bg-tfl10-acc/10" type="text" id="tfl-10-first" name="given-name" placeholder=" " autocomplete="given-name">
<label class="pointer-events-none absolute left-3.5 top-1/2 origin-left -translate-y-1/2 text-base text-tfl10-mut transition-[translate,scale,color] duration-[180ms] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:animate-tfl10-swap peer-focus:text-tfl10-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:animate-tfl10-swap motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-10-first">First name</label>
</div>
<div class="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:origin-center after:scale-x-0 after:bg-tfl10-acc after:transition-[scale] after:duration-[250ms] after:ease-out after:content-[''] focus-within:after:scale-x-100 motion-reduce:after:transition-none">
<input class="peer h-14 w-full rounded-t-[10px] border-b-2 border-tfl10-line bg-tfl10-acc/7 px-3.5 pt-[22px] pb-1.5 text-base text-tfl10-ink outline-hidden transition-[background-color] duration-200 hover:bg-tfl10-acc/11 focus:bg-tfl10-acc/10" type="email" id="tfl-10-mail" name="email" placeholder=" " autocomplete="email">
<label class="pointer-events-none absolute left-3.5 top-1/2 origin-left -translate-y-1/2 text-base text-tfl10-mut transition-[translate,scale,color] duration-[180ms] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:animate-tfl10-swap peer-focus:text-tfl10-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 peer-[:not(:placeholder-shown)]:animate-tfl10-swap motion-reduce:animate-none! motion-reduce:transition-none" for="tfl-10-mail">Email address</label>
</div>
<button class="min-h-12 cursor-pointer rounded-xl bg-tfl10-ink text-[15px] font-semibold 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-tfl10-acc/60" type="button">Subscribe</button>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tfl10-bg: #f3f6f6;
--color-tfl10-bg: oklch(0.97 0.004 200);
--color-tfl10-ink: #16232a;
--color-tfl10-ink: oklch(0.24 0.02 225);
--color-tfl10-mut: #56696f;
--color-tfl10-mut: oklch(0.49 0.025 215);
--color-tfl10-line: #a9b8bf;
--color-tfl10-line: oklch(0.77 0.02 220);
--color-tfl10-acc: #0f7f98;
--color-tfl10-acc: oklch(0.53 0.1 215);
--font-tfl10-sans: 'Instrument Sans', system-ui, sans-serif;
--font-tfl10-serif: 'Instrument Serif', Georgia, serif;
--animate-tfl10-swap: tfl10-swap 0.32s both;
@keyframes tfl10-swap {
0% {
opacity: 1;
translate: 0 -50%;
scale: 1;
}
45% {
opacity: 0;
translate: 0 calc(-50% + 5px);
scale: 1;
}
55% {
opacity: 0;
translate: 0 calc(-50% - 18px);
scale: 0.72;
}
100% {
opacity: 1;
translate: 0 calc(-50% - 13px);
scale: 0.72;
}
}
}
.tfl-10 {
width: 100%;
min-height: 100svh;
}
```How this works
Keyframes speak the individual properties too. Because v4 floats with the translate and scale properties, the swap keyframes animate those same properties and end on the floated values — so when the animation's fill ends, the transition has nothing to fight.
@keyframes tfl10-swap {
45% { opacity: 0; translate: 0 calc(-50% + 5px); }
55% { opacity: 0; translate: 0 calc(-50% - 18px); scale: .72; } … }The underline sits on the border line as the field's after:, so height never changes — zero layout shift on focus.
Make it yours
- Wipe left-to-right:
after:origin-left. - Tint strength:
/7at rest,/11on hover,/10on focus. - Square the top:
rounded-t-none. - Classic slide instead of blink: delete the two
animate-tfl10-swapclasses.
Gotchas — read before shipping
- Animate the underline with
scale-x, neverw-*— width runs on the main thread. - Keep the resting
border-b-2; without it the field reads as plain text. - The tint must keep input text at AA — check tint + ink together.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Keyframes on individual transform properties are supported everywhere Tailwind v4 is.