Floating label input

Free · MIT

Input 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.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/floating-label-input.json

The 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

Use it for
  • 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.
Reach for something else
  • 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 needUse
Single-line text, email, numbersFloatingLabelInput
Multi-line notes with a limitFloatingLabelTextarea with maxLength (counter appears)
Money or unitsprefix="$" and/or suffix="/hr" (shown once the label floats)
Search-like fields people often resetclearable (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

PropTypeDefault
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

PropTypeDefault
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

TabMoves 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