Pure TailwindMIT licensed

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.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="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;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-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;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=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 modifier bg-tfl10-acc/7, so hover and focus tints are one character each.
  • The swap keyframes were rewritten on translate/scale instead of transform to 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 #b9c6cc to meet 3:1 non-text contrast.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind 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: /7 at rest, /11 on hover, /10 on focus.
  • Square the top: rounded-t-none.
  • Classic slide instead of blink: delete the two animate-tfl10-swap classes.

Gotchas — read before shipping

  • Animate the underline with scale-x, never w-* — 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

ChromeSafariFirefoxEdge
111+16.4+128+111+

Keyframes on individual transform properties are supported everywhere Tailwind v4 is.

Techniques used in this demo

Plain CSSno Tailwind surface

Search CodeFronts

Loading…