Floating label input

Inspect

An Input whose label starts where the placeholder would sit and, on focus or once the field holds a value, shrinks into a band at the top of the same control. It never leaves the box, so a field occupies exactly its own height. Unlike a placeholder the label never disappears, so the field stays readable while it is being filled in.

It is an Input — same variants, background, focus ring and invalid halo, same props. Only the label and the optional trailing slot are added, so everything else composes the way it does for a plain Input. One deliberate exception: at each size it stands taller than the plain control (44/48/56 vs 40/44/52), because it has to hold two comfortable lines where a placeholder field holds one.

Use it where vertical space is tight and a stacked <Label> + Input would cost a row: address forms, checkout, onboarding steps. For everything else, prefer the plain Input inside a Field.

Examples

Basic

Inspect

Sizes

Same variant and appearance props as Input — but this one defaults to lg, not md: it is the control our long forms are built from, and lg is the size those forms use. The label wears the control's own text size, so at rest it is pixel-identical to the placeholder it stands in for; raised, it shrinks to 75% of that size.

Inspect

Appearance

Two surfaces, same as Input: the default grey for fields sitting on a card or a white page, and appearance="white" — the white card with the cüte shadow — for the auth shell's grey background. Every text field on the join screens is the white one; a grey field there is a bug (see the join-flow doc).

Inspect

Default — on a card or white page

White — on the auth shell's grey background

Trailing icons

Whatever you pass to trailing is rendered in a real InputGroupAddon with align="inline-end", so it behaves exactly as it would in any other input group: correct inset, icons sized to the group, click-to-focus, and a <InputGroupButton> staying clickable. Nothing is overridden here — pass InputGroupButton and InputGroupText the way you would anywhere else, and let the addon size bare icons for you.

Inspect
⌘K

Required and optional

Same convention as Input: a Required badge, "Optional" as prose. required renders the badge inside the label, so it rides up with it instead of costing the extra row a floating label exists to save — and it still sets native validation and aria-required on the control.

Optional fields say so in a FieldDescription, which does cost a row. If a form is mostly optional fields, that is the wrong trade: use a plain Input with a stacked label there.

Inspect

Optional — for business invoices.

Invalid, disabled, read-only

Exactly the Input states. aria-invalid paints the halo and turns the label destructive; the message is a FieldError, never an InlinePrompt.

Inspect

Forms

It spreads native input props, so react-hook-form's register works directly:

<Field orientation="vertical">
  <FloatingLabelInput
    label="Street and house number"
    aria-invalid={!!errors.street}
    {...register("street")}
  />
  <FieldError errors={[errors.street]} />
</Field>

Notes

  • label is the accessible name — do not add a separate <Label>.
  • The label never renders outside the control, so no space is reserved above it and rows stack on their own gap alone.
  • Uncontrolled fields are read off the DOM through input/change listeners, so browser autofill and form.reset() move the label too.
  • A pre-filled field renders with the label already raised, without animating.
  • The lift is skipped under prefers-reduced-motion.
  • A placeholder only appears once the label is out of the way.

API

Input's props, plus:

PropTypeDescription
labelstringThe floating label. Doubles as the control's accessible name.
trailingReactNodeIcons, a spinner or a small button pinned to the control's right edge. The value's text is inset to clear it.

On this page