Pure TailwindMIT licensed

Tailwind Floating Label Input

The canonical floating label, and the demo that teaches the whole collection: peer on the input, peer-focus: and peer-[:not(:placeholder-shown)]: on the label. The CSS original writes two selector blocks that live nowhere near the label; here the label owns both of its lifted states.

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

<div class="tif-01 grid min-h-svh w-full place-items-center bg-tif01-bg px-6 py-14 font-tif01-sans" aria-label="Floating label input demo">
  <label class="relative block w-full max-w-70">
    <input type="email" name="tif01-email" autocomplete="email" placeholder=" " required
      class="peer w-full rounded-xl border border-white/12 bg-tif01-field px-3.5 pt-4.5 pb-2 text-sm text-tif01-ink outline-none transition-[border-color,background-color] duration-200 focus:border-tif01-acc2 focus:bg-tif01-field2">
    <span class="pointer-events-none absolute top-3.5 left-3.5 text-[13px] tracking-normal text-tif01-mut transition-[transform,font-size,color,letter-spacing] duration-200 peer-focus:-translate-y-2.5 peer-focus:text-[10px] peer-focus:tracking-[0.06em] peer-focus:text-tif01-acc peer-focus:uppercase peer-[:not(:placeholder-shown)]:-translate-y-2.5 peer-[:not(:placeholder-shown)]:text-[10px] peer-[:not(:placeholder-shown)]:tracking-[0.06em] peer-[:not(:placeholder-shown)]:text-tif01-acc peer-[:not(:placeholder-shown)]:uppercase motion-reduce:transition-none">Email address</span>
  </label>
</div>
@import "tailwindcss";

@theme {
  --color-tif01-bg: #12121a;
  --color-tif01-bg: oklch(0.19 0.015 285);
  --color-tif01-field: #1a1a22;
  --color-tif01-field: oklch(0.23 0.015 285);
  --color-tif01-field2: #1f1f2a;
  --color-tif01-field2: oklch(0.26 0.018 285);
  --color-tif01-ink: #f0eeff;
  --color-tif01-ink: oklch(0.95 0.02 290);
  --color-tif01-mut: #b8b6d4;
  --color-tif01-mut: oklch(0.77 0.03 290);
  --color-tif01-acc: #a78bfa;
  --color-tif01-acc: oklch(0.74 0.15 295);
  --color-tif01-acc2: #7c6cff;
  --color-tif01-acc2: oklch(0.62 0.2 288);
  --font-tif01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Stage contract, stated in CSS as well as in the markup, so the demo is sized
   before the browser build compiles. */

.tif-01 {
  width: 100%;
}

.tif-01 ::selection {
  background: var(--color-tif01-acc);
  color: #12121a;
}

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-tif01-bg: #12121a;
  --color-tif01-bg: oklch(0.19 0.015 285);
  --color-tif01-field: #1a1a22;
  --color-tif01-field: oklch(0.23 0.015 285);
  --color-tif01-field2: #1f1f2a;
  --color-tif01-field2: oklch(0.26 0.018 285);
  --color-tif01-ink: #f0eeff;
  --color-tif01-ink: oklch(0.95 0.02 290);
  --color-tif01-mut: #b8b6d4;
  --color-tif01-mut: oklch(0.77 0.03 290);
  --color-tif01-acc: #a78bfa;
  --color-tif01-acc: oklch(0.74 0.15 295);
  --color-tif01-acc2: #7c6cff;
  --color-tif01-acc2: oklch(0.62 0.2 288);
  --font-tif01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Stage contract, stated in CSS as well as in the markup, so the demo is sized
   before the browser build compiles. */

.tif-01 {
  width: 100%;
}

.tif-01 ::selection {
  background: var(--color-tif01-acc);
  color: #12121a;
}
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=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap">

What changed in the port

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

  • Two selector blocks (input:focus + .label and input:not(:placeholder-shown) + .label) collapse into one class list on the label itself.
  • The three separate transition declarations become one transition-[…] per element, so they cannot drift out of lockstep.
  • @media (prefers-reduced-motion) was absent from the original; it is one utility here, so there is no excuse.
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 Floating Label Input
Source: https://codefronts.com/components/tailwind-css-input-fields/tailwind-floating-label-input/

The canonical floating label, and the demo that teaches the whole collection: peer on the input, peer-focus: and peer-[:not(:placeholder-shown)]: on the label. The CSS original writes two selector blocks that live nowhere near the label; here the label owns both of its lifted states.
## HTML
```html
<div class="tif-01 grid min-h-svh w-full place-items-center bg-tif01-bg px-6 py-14 font-tif01-sans" aria-label="Floating label input demo">
  <label class="relative block w-full max-w-70">
    <input type="email" name="tif01-email" autocomplete="email" placeholder=" " required
      class="peer w-full rounded-xl border border-white/12 bg-tif01-field px-3.5 pt-4.5 pb-2 text-sm text-tif01-ink outline-none transition-[border-color,background-color] duration-200 focus:border-tif01-acc2 focus:bg-tif01-field2">
    <span class="pointer-events-none absolute top-3.5 left-3.5 text-[13px] tracking-normal text-tif01-mut transition-[transform,font-size,color,letter-spacing] duration-200 peer-focus:-translate-y-2.5 peer-focus:text-[10px] peer-focus:tracking-[0.06em] peer-focus:text-tif01-acc peer-focus:uppercase peer-[:not(:placeholder-shown)]:-translate-y-2.5 peer-[:not(:placeholder-shown)]:text-[10px] peer-[:not(:placeholder-shown)]:tracking-[0.06em] peer-[:not(:placeholder-shown)]:text-tif01-acc peer-[:not(:placeholder-shown)]:uppercase motion-reduce:transition-none">Email address</span>
  </label>
</div>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tif01-bg: #12121a;
  --color-tif01-bg: oklch(0.19 0.015 285);
  --color-tif01-field: #1a1a22;
  --color-tif01-field: oklch(0.23 0.015 285);
  --color-tif01-field2: #1f1f2a;
  --color-tif01-field2: oklch(0.26 0.018 285);
  --color-tif01-ink: #f0eeff;
  --color-tif01-ink: oklch(0.95 0.02 290);
  --color-tif01-mut: #b8b6d4;
  --color-tif01-mut: oklch(0.77 0.03 290);
  --color-tif01-acc: #a78bfa;
  --color-tif01-acc: oklch(0.74 0.15 295);
  --color-tif01-acc2: #7c6cff;
  --color-tif01-acc2: oklch(0.62 0.2 288);
  --font-tif01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Stage contract, stated in CSS as well as in the markup, so the demo is sized
   before the browser build compiles. */

.tif-01 {
  width: 100%;
}

.tif-01 ::selection {
  background: var(--color-tif01-acc);
  color: #12121a;
}
```

How this works

The label states its own lifted geometry. Both triggers — focused and filled — resolve to the same four utilities, written once on the element that moves:

class="absolute top-3.5 left-3.5 text-[13px]
  peer-focus:-translate-y-2.5 peer-focus:text-[10px]
  peer-[:not(:placeholder-shown)]:-translate-y-2.5 …"

The peer must precede the label in the DOM, because peer-* compiles to a general-sibling selector. That is the same constraint the CSS version has with +, but stated on the element rather than in a stylesheet — reorder the markup and the class simply stops applying, in view, where you can see it.

Make it yours

  • Accent: --color-tif01-acc (lifted label) and --color-tif01-acc2 (focus border) in @theme — they work as a light/dark pair.
  • Lift distance: -translate-y-2.5, paired with the input's pt-4.5 pb-2 so text never collides with the label.
  • Motion: one duration-200 on the input and one on the label. Keep them equal.
  • Sentence case: drop peer-focus:uppercase and bump text-[10px] to text-[11px].

Gotchas — read before shipping

  • The placeholder must be a single space. Remove it and :placeholder-shown never matches, so the label sits lifted forever.
  • peer-* is a sibling selector: the label must come after the input and share a parent. Wrapping either kills the lift silently.
  • Chrome autofill does not always re-evaluate :placeholder-shown — test with a saved credential before shipping a login form.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

:placeholder-shown is Baseline; the floor quoted is Tailwind v4's generated sheet.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…