Pure TailwindMIT licensed

Tailwind Staggered Letter-Cascade Floating Label

Each letter of the label rises on its own, a left-to-right wave: one *: class list styles every letter span, *:[transition-delay:calc(var(--i)*30ms)] staggers them, and group-focus-within: + group-has-[input:not(:placeholder-shown)]: fire the float. The real label text sits in an sr-only span so screen readers hear a word, not letters.

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-01 w-full min-h-svh grid place-items-center bg-tfl01-bg bg-[radial-gradient(90%_70%_at_50%_0%,var(--color-tfl01-glow),transparent_70%)] px-5 py-12 font-tfl01-sans" aria-label="Staggered letter cascade floating label demo">
  <form class="flex w-full max-w-[380px] flex-col gap-5 rounded-3xl bg-white p-8 shadow-[0_24px_60px_-32px_oklch(0.35_0.12_320/0.45)] ring-1 ring-tfl01-ink/5" novalidate>
    <div class="mb-1 grid gap-1">
      <h3 class="text-2xl font-extrabold tracking-[-0.02em] text-tfl01-ink">Join the studio</h3>
      <p class="text-sm text-tfl01-mut">Focus a field and watch the label ripple.</p>
    </div>
    <div class="group relative">
      <input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl01-line bg-white px-4 pt-[22px] pb-1.5 text-base text-tfl01-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl01-line-hover focus:border-tfl01-acc focus:ring-4 focus:ring-tfl01-acc/15" type="text" id="tfl-01-name" name="name" placeholder=" " autocomplete="name">
      <label class="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-base text-tfl01-mut *:inline-block *:origin-bottom-left *:transition-[translate,scale,color] *:duration-[340ms] *:ease-[cubic-bezier(.3,1.6,.4,1)] *:[transition-delay:calc(var(--i)*30ms)] group-focus-within:*:-translate-y-3.5 group-focus-within:*:scale-72 group-focus-within:*:text-tfl01-acc group-has-[input:not(:placeholder-shown)]:*:-translate-y-3.5 group-has-[input:not(:placeholder-shown)]:*:scale-72 motion-reduce:*:[transition-delay:0s]" for="tfl-01-name">
        <span class="sr-only">Your name</span>
        <span aria-hidden="true" style="--i:0">Y</span><span aria-hidden="true" style="--i:1">o</span><span aria-hidden="true" style="--i:2">u</span><span aria-hidden="true" style="--i:3">r</span><span aria-hidden="true" style="--i:4">&nbsp;</span><span aria-hidden="true" style="--i:5">n</span><span aria-hidden="true" style="--i:6">a</span><span aria-hidden="true" style="--i:7">m</span><span aria-hidden="true" style="--i:8">e</span>
      </label>
    </div>
    <div class="group relative">
      <input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl01-line bg-white px-4 pt-[22px] pb-1.5 text-base text-tfl01-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl01-line-hover focus:border-tfl01-acc focus:ring-4 focus:ring-tfl01-acc/15" type="email" id="tfl-01-mail" name="email" placeholder=" " autocomplete="email">
      <label class="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-base text-tfl01-mut *:inline-block *:origin-bottom-left *:transition-[translate,scale,color] *:duration-[340ms] *:ease-[cubic-bezier(.3,1.6,.4,1)] *:[transition-delay:calc(var(--i)*30ms)] group-focus-within:*:-translate-y-3.5 group-focus-within:*:scale-72 group-focus-within:*:text-tfl01-acc group-has-[input:not(:placeholder-shown)]:*:-translate-y-3.5 group-has-[input:not(:placeholder-shown)]:*:scale-72 motion-reduce:*:[transition-delay:0s]" for="tfl-01-mail">
        <span class="sr-only">Email</span>
        <span aria-hidden="true" style="--i:0">E</span><span aria-hidden="true" style="--i:1">m</span><span aria-hidden="true" style="--i:2">a</span><span aria-hidden="true" style="--i:3">i</span><span aria-hidden="true" style="--i:4">l</span>
      </label>
    </div>
    <button class="mt-1 min-h-12 cursor-pointer rounded-xl bg-tfl01-acc text-[15px] font-bold text-white shadow-[0_10px_24px_-12px_var(--color-tfl01-acc)] transition-[filter,scale] duration-150 hover:brightness-110 active:scale-98 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tfl01-acc/50" type="button">Apply</button>
  </form>
</section>
@import "tailwindcss";

@theme {
  --color-tfl01-bg: #f6eff6;
  --color-tfl01-bg: oklch(0.96 0.012 320);
  --color-tfl01-glow: #fff9ff;
  --color-tfl01-glow: oklch(0.99 0.008 320);
  --color-tfl01-ink: #241a24;
  --color-tfl01-ink: oklch(0.23 0.025 320);
  --color-tfl01-mut: #6f5e73;
  --color-tfl01-mut: oklch(0.5 0.03 315);
  --color-tfl01-line: #e3d5e3;
  --color-tfl01-line: oklch(0.88 0.02 320);
  --color-tfl01-line-hover: #c9b5ca;
  --color-tfl01-line-hover: oklch(0.79 0.03 320);
  --color-tfl01-acc: #b43aa9;
  --color-tfl01-acc: oklch(0.55 0.19 325);
  --font-tfl01-sans: 'Bricolage Grotesque', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tfl-01 {
  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-tfl01-bg: #f6eff6;
  --color-tfl01-bg: oklch(0.96 0.012 320);
  --color-tfl01-glow: #fff9ff;
  --color-tfl01-glow: oklch(0.99 0.008 320);
  --color-tfl01-ink: #241a24;
  --color-tfl01-ink: oklch(0.23 0.025 320);
  --color-tfl01-mut: #6f5e73;
  --color-tfl01-mut: oklch(0.5 0.03 315);
  --color-tfl01-line: #e3d5e3;
  --color-tfl01-line: oklch(0.88 0.02 320);
  --color-tfl01-line-hover: #c9b5ca;
  --color-tfl01-line-hover: oklch(0.79 0.03 320);
  --color-tfl01-acc: #b43aa9;
  --color-tfl01-acc: oklch(0.55 0.19 325);
  --font-tfl01-sans: 'Bricolage Grotesque', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tfl-01 {
  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=Bricolage+Grotesque:opsz,wght@12..96,400..800&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • Nine .flb-01__ch class attributes collapsed into one *: class list on the label — the letters now carry only their --i index.
  • The sibling selector input:focus ~ label .ch became group-focus-within:*: on the field, because peer-* cannot reach grandchildren.
  • UI upgrade from the CSS original: the resting label rose from #96829b (3.6:1, failing) to oklch(0.5) (5.6:1), the accent darkened so white button text passes AA, and the form gained a one-line instruction so the effect is discoverable.
  • UI upgrade: Bricolage Grotesque replaces Segoe UI; outline-hidden replaces outline:none, so Windows High Contrast still paints a focus outline.
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 Staggered Letter-Cascade Floating Label
Source: https://codefronts.com/components/tailwind-css-floating-label-inputs/tailwind-staggered-letter-cascade-floating-label/

Each letter of the label rises on its own, a left-to-right wave: one *: class list styles every letter span, *:[transition-delay:calc(var(--i)*30ms)] staggers them, and group-focus-within: + group-has-[input:not(:placeholder-shown)]: fire the float. The real label text sits in an sr-only span so screen readers hear a word, not letters.
## HTML
```html
<section class="tfl-01 w-full min-h-svh grid place-items-center bg-tfl01-bg bg-[radial-gradient(90%_70%_at_50%_0%,var(--color-tfl01-glow),transparent_70%)] px-5 py-12 font-tfl01-sans" aria-label="Staggered letter cascade floating label demo">
  <form class="flex w-full max-w-[380px] flex-col gap-5 rounded-3xl bg-white p-8 shadow-[0_24px_60px_-32px_oklch(0.35_0.12_320/0.45)] ring-1 ring-tfl01-ink/5" novalidate>
    <div class="mb-1 grid gap-1">
      <h3 class="text-2xl font-extrabold tracking-[-0.02em] text-tfl01-ink">Join the studio</h3>
      <p class="text-sm text-tfl01-mut">Focus a field and watch the label ripple.</p>
    </div>
    <div class="group relative">
      <input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl01-line bg-white px-4 pt-[22px] pb-1.5 text-base text-tfl01-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl01-line-hover focus:border-tfl01-acc focus:ring-4 focus:ring-tfl01-acc/15" type="text" id="tfl-01-name" name="name" placeholder=" " autocomplete="name">
      <label class="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-base text-tfl01-mut *:inline-block *:origin-bottom-left *:transition-[translate,scale,color] *:duration-[340ms] *:ease-[cubic-bezier(.3,1.6,.4,1)] *:[transition-delay:calc(var(--i)*30ms)] group-focus-within:*:-translate-y-3.5 group-focus-within:*:scale-72 group-focus-within:*:text-tfl01-acc group-has-[input:not(:placeholder-shown)]:*:-translate-y-3.5 group-has-[input:not(:placeholder-shown)]:*:scale-72 motion-reduce:*:[transition-delay:0s]" for="tfl-01-name">
        <span class="sr-only">Your name</span>
        <span aria-hidden="true" style="--i:0">Y</span><span aria-hidden="true" style="--i:1">o</span><span aria-hidden="true" style="--i:2">u</span><span aria-hidden="true" style="--i:3">r</span><span aria-hidden="true" style="--i:4">&nbsp;</span><span aria-hidden="true" style="--i:5">n</span><span aria-hidden="true" style="--i:6">a</span><span aria-hidden="true" style="--i:7">m</span><span aria-hidden="true" style="--i:8">e</span>
      </label>
    </div>
    <div class="group relative">
      <input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl01-line bg-white px-4 pt-[22px] pb-1.5 text-base text-tfl01-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl01-line-hover focus:border-tfl01-acc focus:ring-4 focus:ring-tfl01-acc/15" type="email" id="tfl-01-mail" name="email" placeholder=" " autocomplete="email">
      <label class="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-base text-tfl01-mut *:inline-block *:origin-bottom-left *:transition-[translate,scale,color] *:duration-[340ms] *:ease-[cubic-bezier(.3,1.6,.4,1)] *:[transition-delay:calc(var(--i)*30ms)] group-focus-within:*:-translate-y-3.5 group-focus-within:*:scale-72 group-focus-within:*:text-tfl01-acc group-has-[input:not(:placeholder-shown)]:*:-translate-y-3.5 group-has-[input:not(:placeholder-shown)]:*:scale-72 motion-reduce:*:[transition-delay:0s]" for="tfl-01-mail">
        <span class="sr-only">Email</span>
        <span aria-hidden="true" style="--i:0">E</span><span aria-hidden="true" style="--i:1">m</span><span aria-hidden="true" style="--i:2">a</span><span aria-hidden="true" style="--i:3">i</span><span aria-hidden="true" style="--i:4">l</span>
      </label>
    </div>
    <button class="mt-1 min-h-12 cursor-pointer rounded-xl bg-tfl01-acc text-[15px] font-bold text-white shadow-[0_10px_24px_-12px_var(--color-tfl01-acc)] transition-[filter,scale] duration-150 hover:brightness-110 active:scale-98 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tfl01-acc/50" type="button">Apply</button>
  </form>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tfl01-bg: #f6eff6;
  --color-tfl01-bg: oklch(0.96 0.012 320);
  --color-tfl01-glow: #fff9ff;
  --color-tfl01-glow: oklch(0.99 0.008 320);
  --color-tfl01-ink: #241a24;
  --color-tfl01-ink: oklch(0.23 0.025 320);
  --color-tfl01-mut: #6f5e73;
  --color-tfl01-mut: oklch(0.5 0.03 315);
  --color-tfl01-line: #e3d5e3;
  --color-tfl01-line: oklch(0.88 0.02 320);
  --color-tfl01-line-hover: #c9b5ca;
  --color-tfl01-line-hover: oklch(0.79 0.03 320);
  --color-tfl01-acc: #b43aa9;
  --color-tfl01-acc: oklch(0.55 0.19 325);
  --font-tfl01-sans: 'Bricolage Grotesque', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tfl-01 {
  width: 100%;
  min-height: 100svh;
}
```

How this works

Why group, not peer. peer-* only reaches siblings of the input; the letters are grandchildren of the label. The field wrapper becomes the group, and two variants cover both float triggers:

group-focus-within:*:-translate-y-3.5
group-has-[input:not(:placeholder-shown)]:*:-translate-y-3.5

The child variant does the repetition. The CSS original put a class on all nine letter spans; *: targets direct children, so the letters carry only style="--i:N".

The spring is *:ease-[cubic-bezier(.3,1.6,.4,1)] — the letters overshoot and settle, and the un-float plays the wave back automatically.

Make it yours

  • Wave speed: the 30ms multiplier in *:[transition-delay:…].
  • Bounce: the second bezier value (1.6) — higher is springier.
  • Reverse the wave by numbering --i right-to-left.
  • Re-theme: edit --color-tfl01-acc; letters, ring and button follow.

Gotchas — read before shipping

  • The letter run MUST be aria-hidden="true" with the real text in sr-only, or screen readers spell it out.
  • Keep labels under ~15 characters; cap total delay under ~450ms.
  • Spaces collapse inside inline-block spans — use &nbsp; for the gap.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

:has() and the *: child variant are within Tailwind v4's own floor. Under reduced motion, delays zero out and all letters float together.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…