Floating label input
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
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.
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).
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.
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.
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.
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
labelis 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/changelisteners, so browser autofill andform.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
placeholderonly appears once the label is out of the way.
API
Input's props, plus:
| Prop | Type | Description |
|---|---|---|
| label | string | The floating label. Doubles as the control's accessible name. |
| trailing | ReactNode | Icons, a spinner or a small button pinned to the control's right edge. The value's text is inset to clear it. |