16 Tailwind CSS floating label inputs for login and password fields, checkout address forms, search bars, select menus, and auto-growing textareas — spanning the pure peer pattern, Material outlined and filled fields, validation states, Chrome autofill fixes, ARIA-audited accessibility, icon-inside layouts, dark mode with light-dark(), and no-zoom mobile sizing. Scoped under .tfl-NN for no-collision pasting, prefers-reduced-motion guarded, framework-agnostic.
Building a whole Tailwind app, not just this component?
You just browsed 16 Tailwind floating label inputs. Untitled UI Pro unlocks 5,000+ components and sections and 250+ page examples for React, styled with Tailwind CSS v4. One licence is a one-time $349 with lifetime updates; teams start at $999.
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.
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.
A comet of light orbits the field's border while the label floats into a glowing chip. The beam is a before: layer painting bg-[conic-gradient(from_var(--tfl03-a),…)]; the @property registration that makes the angle animatable is the one rule that stays CSS. The orbit runs only under focus-within: — zero idle cost.
The canonical floating label in its native Tailwind form: peer on the input, and peer-focus: plus peer-[:not(:placeholder-shown)]: on the label. The label floats with translate-y-[calc(-50%_-_13px)] scale-72 — transform-only, zero listeners, zero hydration. Every other demo in the collection builds on this one.
Chrome autofills and the label crashes through the value — unless peer-autofill: joins the float. The port adds peer-[:-webkit-autofill]: as its own utility (so an unknown selector can never kill the list), re-skins the yellow wash with autofill:shadow-[inset_0_0_0_1000px_…], and floats the label out of the field into an eyebrow above it.
CSS-only validation that waits for the user: peer-user-invalid: turns border, label and message red only after interaction, has-[input:user-invalid]:animate-tfl06-shake nudges the field, and has-[input:user-valid]:after:opacity-100 reveals a check chip. Native constraint attributes do all the checking; the label flips on a 3D rotateX as it floats.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
The floating label audited: real for/id binding, aria-describedby hints, a required marker that isn't colour-only, AA contrast in both label states, and a thick focus-visible:outline-3 outline-offset-3 ring. The float barely shrinks (scale-85), bolds instead, and grows an accent tick via peer-focus:before:w-3 — low-vision users never lose it.
<select> has no placeholder, so the port uses the constraint API: a required select with an empty first option is :invalid until a real choice is made, and peer-valid: floats the label into a solid accent pill on the top border. The chevron is a rotated border square that flips with peer-focus:rotate-225.
The Google-style outlined field: on focus the label glides up onto the border and the outline 'opens' a notch around it. The notch is the label's own bg-white px-[5px] erasing the border behind it — no fieldset, no legend, no JS measuring. Focus thickens the edge with focus:shadow-[inset_0_0_0_1px_…], so the text never shifts.
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.
One set of tokens, two themes: every colour in @theme is a light-dark() pair, so the utilities themselves are theme-agnostic — bg-tfl11-card is white or ink depending on color-scheme. A role="switch" checkbox flips it with has-[#tfl-11-dark:checked]:scheme-dark on the root. No dark: variants anywhere.
Search, email and username fields where the inline SVG icon never collides with the label: at rest the label sits beside the icon; on focus the icon spins out (group-focus-within:-rotate-25 group-focus-within:opacity-0) and the label slides left and up into its place with translate-x-[-30px]. One focus-within:text-tfl12-acc on the field tints icon, border and label together.
The mobile survival kit, now sized to the form rather than the screen: the form is an @container, and heights, paddings, type and the float distance are clamp()s over cqi container units. Input text is floored at 16px so iOS Safari never zoom-jumps, every target is ≥48px, and a tinted highlight lane gives the floated label its own track.
Floating labels on multi-line input, solved: the label anchors to the textarea's first text line (top-[26px], not top-1/2), floats out into a corner-tab chip on the top-left edge, and the field grows with its content via v4's field-sizing-content between min-h-30 and max-h-80 — no autosize library, no listeners.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
The login-form workhorse, finished: a floating-label password field with a 44px eye <button> whose icons swap via group-aria-pressed/eye:, plus the strength meter the CSS original only described — four segments lit by group-data-[strength=N]/pw: variants. The script flips attributes; every visual lives in utilities. The label springs with an overshoot bezier.
A checkout address form laying floating-label fields onto a six-track grid: col-span-3 pairs, a full-width street, and a col-span-2 City/State/ZIP row. Named container queries (@container/form + @max-[26rem]/form:) re-flow it by the form's own width, and floated labels snap to uppercase micro-legends that unify the grid.
FAQ
Frequently asked questions
How does a Tailwind floating label work without any JavaScript?
The mechanism is the peer class plus sibling state variants — no script runs at all. You mark the <input> as peer, then style the label with two variants: peer-placeholder-shown: (the resting state, label sitting inside the field) and peer-focus: (the floated state, label small and lifted). The trick that makes it purely CSS is giving the input a single-space placeholder, placeholder=" ". That keeps :placeholder-shown true only while the field is genuinely empty, so the label drops when there is a value and floats when the field is focused or filled. Demo 04 is the canonical form of this. Because everything is a state variant compiled to a real CSS selector, the label animates on focus, blur, autofill and back-button restore with zero runtime cost — the browser drives it entirely through the :placeholder-shown and :focus pseudo-classes.
Why does my floating label sit on top of the value after Chrome autofills the field?
This is the most common floating-label bug and it is not your CSS logic — it is timing. Chrome's autofill writes a value into the input without firing the events that change :placeholder-shown in the frame you expect, and the browser's own autofill styling repaints the field. Your label is still in its resting peer-placeholder-shown: position, so it crashes straight through the autofilled text. The fix is to add the autofill state as a second trigger for the floated position. Demo 05 does exactly this: it joins peer-autofill: to the same rules that peer-focus: already carries, so an autofilled field lifts the label just like a focused or typed-in one. Pair it with the usual peer-placeholder-shown: resting rule and the label now knows three ways to be "occupied": focused, filled by typing, or filled by autofill. Test it by saving a login and reloading — the label should already be floated on paint.
How do I make a floating label accessible to screen readers?
A floated label is still a real label — it must be programmatically bound, not just visually associated. Demo 07 wires it correctly: the <label> carries a for that matches the input's id, so clicking the label focuses the field and assistive tech announces the name. Hint and error text live in their own element referenced by aria-describedby, so the description is read after the label rather than being lost as decoration. Two things to watch. First, never rely on the placeholder as the accessible name — the single-space placeholder the float mechanism needs is intentionally empty, so the <label> must carry the text. Second, do not animate the label so small it fails contrast or so fast it violates reduced-motion expectations; keep the floated size legible and gate any transition behind motion-reduce:. The label element stays in the DOM the whole time, so its accessible name is stable whether floated or resting.
Can I use a floating label on a select dropdown the same way?
Not with the placeholder trick, because <select> has no placeholder attribute — :placeholder-shown never applies, so the usual peer-placeholder-shown: rule has nothing to hook onto. Demo 08 solves it with the constraint-validation API instead. The select is marked required and its first <option> is an empty-value placeholder option, which means the control is :invalid while nothing real is chosen and :valid once the user picks an option. You then drive the label from validity: a peer-invalid: variant holds the resting position, and the field floating up on peer-focus: or once it becomes peer-valid:. The one gotcha is that a native select is always "filled" visually, so you must keep that first empty option present and never give it a real value — otherwise the field reads as valid immediately and the label floats before the user has chosen anything.
How do I stop iOS from zooming in when the user taps a floating label input?
iOS Safari auto-zooms whenever a focused input's font size is below 16px, which yanks the whole layout around and is jarring on a form. The rule is simply that the input's rendered font size must be at least 16px at the moment of focus — a 14px field will zoom no matter what else you do. Demo 13 sets the field type at 16px and, crucially, sizes the form to itself with @container rather than the viewport, so the breakpoints that shrink padding and spacing on a narrow screen never drag the input's font below the zoom threshold. Keep any responsive type scaling on the label and surrounding chrome, not on the input text. If you must show a visually smaller field, scale it with the box and letter-spacing but hold the font-size at 16px. This also means testing on a real device or the iOS simulator — desktop Chrome will never reproduce the zoom, so a passing desktop check tells you nothing here.
How do I show error and success states on a floating label without JavaScript?
Demo 06 does this with the user-interaction validity pseudo-classes, which are the key to not shouting at someone mid-typing. A plain :invalid selector is true the instant an empty required field renders, so a naive rule paints every field red before the user has touched anything. Instead it uses peer-user-invalid: and peer-user-valid:, which only apply after the field has been interacted with and blurred. So the border, the floated label and the helper message all turn red on peer-user-invalid: once the user leaves an invalid field, and green on peer-user-valid: when the value satisfies the field's pattern or type. Wire the message element through aria-describedby so the state change is announced, not just coloured. The whole thing is CSS driven by the input's own constraint attributes — required, type="email", pattern — so there is no validation code to keep in sync.
How do I keep a floating label from colliding with an icon inside the input?
The collision happens because both the icon and the label want the same left edge of the field. Demo 12 fixes the geometry rather than fighting it. The inline SVG icon is absolutely positioned inside the field and given a fixed lane, and the input gets left padding equal to that lane so the typed text starts clear of the icon. The label is then offset by the same amount at rest — its peer-placeholder-shown: position starts to the right of the icon, aligned with where the text will be, so nothing overlaps. When the field floats, the label lifts to the top edge on peer-focus: and can slide back to the box's real left edge, since the icon lane no longer matters up there. Mark the decorative icon aria-hidden so it is not announced. The reusable lesson: pad the input for the icon, offset the resting label by the same distance, and let the floated label ignore the lane entirely.
When should I not use a floating label input?
Floating labels are elegant but they trade away persistent guidance, so skip them where the field needs a hint the user must see while typing. Because the label doubles as the placeholder, once the field is focused the resting text is gone — if you also need format help like "MM/YY" or "include a country code", that has to live in a separate aria-describedby hint, not the label, or the user loses it exactly when they need it. They also compress the label into a small floated size, so long labels truncate; a field that needs a full sentence of context is better with a static label above it. And on a dense multi-column form like the checkout grid in demo 16, sixteen animating labels can read as busy — a plain top-aligned label scans faster there. Rule of thumb: use floating labels for short, self-explanatory fields (email, name, password) where the space saving matters, and fall back to a static <label> above the field whenever the label is long or the field needs ongoing formatting guidance.