Floating label input
Free · MITInput and textarea whose label springs up on focus or value, with prefix/suffix slots that fade in, an animated clear button, character counter and a slide-in error.
Installation
$ npx shadcn@latest add https://goodui.dev/r/floating-label-input.jsonThe file lands in components/goodui/floating-label-input.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/floating-label-input works too.
Usage
import * as React from "react"
import { FloatingLabelInput } from "@/components/goodui/floating-label-input"
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/
export function BillingEmail() {
const [email, setEmail] = React.useState("")
const [touched, setTouched] = React.useState(false)
const error = touched && !EMAIL.test(email) ? "Enter a valid email, like maya@brightclean.co" : undefined
return (
<FloatingLabelInput
label="Billing email"
type="email"
autoComplete="email"
value={email}
onValueChange={setEmail}
onBlur={() => setTouched(true)}
error={error}
description="Invoices and payout receipts go here."
clearable
/>
)
}When to use
- Compact forms (checkout, billing, profile) where a label above every field takes too much room.
- Fields with a unit or fixed part: a currency prefix, "/hr", a domain suffix.
- Notes and messages with a length limit: FloatingLabelTextarea shows a counter that turns amber above 90%.
- Fields that need a clear button and an error that slides in under them.
- Dense settings pages and tables: a plain input with a label above scans faster.
- Several values in one field: use tag-input.
- Validation feedback around an input you already have (shadcn Input, select): use input-shake.
| If you need | Use |
|---|---|
| Single-line text, email, numbers | FloatingLabelInput |
| Multi-line notes with a limit | FloatingLabelTextarea with maxLength (counter appears) |
| Money or units | prefix="$" and/or suffix="/hr" (shown once the label floats) |
| Search-like fields people often reset | clearable (add onClear to react) |
Examples
Rate with prefix and suffix
<FloatingLabelInput label="Hourly rate" inputMode="decimal" prefix="$" suffix="/hr" defaultValue="65" />Notes with a character counter
import { FloatingLabelTextarea } from "@/components/goodui/floating-label-input"
<FloatingLabelTextarea
label="Notes for your cleaner"
placeholder="Gate code, parking, pets, rooms to skip…"
maxLength={160}
clearable
/>API reference
FloatingLabelInput
| Prop | Type | Default |
|---|---|---|
| label* Text that sits inside the field and floats up on focus or when there is a value. | React.ReactNode | — |
| error Error message; slides in under the field and turns it red. | React.ReactNode | — |
| description Helper text shown when there is no error. | React.ReactNode | — |
| clearable Show a clear (×) button when the field has a value. | boolean | — |
| onValueChange Called with the new string on typing and on clear. The clear button also fires `onChange` with a real change event. | (value: string) => void | — |
| onClear Called after the clear button empties the field. | () => void | — |
| inputClassName Classes for the native input/textarea; `className` styles the root. | string | — |
| labels Text for built-in controls and screen readers; pass translations here. | Partial<FloatingLabelInputLabels> | — |
| prefix Shown before the value once the label floats, e.g. "$" or "https://". | React.ReactNode | — |
| suffix Shown after the value once the label floats, e.g. "/hr" or ".com". | React.ReactNode | — |
Also accepts every <input> attribute.
FloatingLabelTextarea
| Prop | Type | Default |
|---|---|---|
| label* Text that sits inside the field and floats up on focus or when there is a value. | React.ReactNode | — |
| error Error message; slides in under the field and turns it red. | React.ReactNode | — |
| description Helper text shown when there is no error. | React.ReactNode | — |
| clearable Show a clear (×) button when the field has a value. | boolean | — |
| onValueChange Called with the new string on typing and on clear. The clear button also fires `onChange` with a real change event. | (value: string) => void | — |
| onClear Called after the clear button empties the field. | () => void | — |
| inputClassName Classes for the native input/textarea; `className` styles the root. | string | — |
| labels Text for built-in controls and screen readers; pass translations here. | Partial<FloatingLabelInputLabels> | — |
Also accepts every <textarea> attribute.
Accessibility
| Tab | Moves from the field to the clear button when it is shown (a native <button>; Enter or Space clears and refocuses the field). |
- The label is a real <label htmlFor> tied to the native input or textarea, so clicking it focuses the field.
- error and description are linked with aria-describedby; the error has role="alert" and sets aria-invalid.
- The textarea counter becomes a polite live region once the value passes 90% of maxLength.
- prefix and suffix are aria-hidden; put the unit in the label too if it matters ("Hourly rate (USD)").
- The clear button's aria-label is labels.clear(label) ("Clear <label>", or "Clear field" via labels.field when label is not a string); pass labels to translate it.
Motion
- The label moves up and scales to 0.78 on a spring (stiffness 520, damping 36) when the field is focused, has a value, or is filled by browser autofill.
- prefix and suffix fade in over 0.15 s once the label floats; the placeholder fades in only while focused.
- The clear button spins in from -90° and scale 0.5 on the same spring; the error slides down and grows to its height in 0.2 s.
- Reduced motion: the label jumps without a spring, the clear button and error only fade.
Notes for AI
Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.
- Prefer onValueChange(value: string). onChange also works: the clear button fires a real input event, so onChange, onValueChange and onClear all run on clear (react-hook-form register works).
- Built-in text comes from the labels prop: clear(label) and field; pass translations there.
- className styles the outer wrapper; use inputClassName for the native input or textarea.
- label is required. placeholder is hidden until the field is focused, so do not rely on it as the label.
- FloatingLabelTextarea has no prefix or suffix; maxLength turns on the counter.
- Pass error only when there is one (undefined otherwise); any truthy error turns the field red and replaces description.
FAQ
Does it work with browser autofill?
Yes for the input. A no-op CSS animation on :-webkit-autofill tells the component the field was filled, so the label floats even before a change event.
Does the textarea grow with its content?
It uses field-sizing: content, so it grows up to max-h-48 in browsers that support it and stays at its minimum height elsewhere.
Related
Six field styles with top, floating, inline or hidden labels, focus ring, glow, underline-grow or lift, error shake with sliding messages, icon and text addons, clear button, counter and auto-growing textarea.
Validation wrapper for any input that shakes and slides in an error with an icon when invalid, and draws a check circle when valid.
Chips input where Enter, comma or paste adds tags that spring in, Backspace arms then removes, duplicates shake, with a filtered suggestions dropdown, validation and a max count.
Search icon that springs open into a field with a recent-searches dropdown and a / shortcut; Esc or blur folds it back when empty.