KQ-1127 · HANG/LABEL

Hang Label

A text field whose label lifts off the placeholder on the glide spring, flipping up on a hinge along its top edge as it travels, and lands on the field's top edge where it hangs and swings on an underdamped pendulum spring whose first swing peaks at the swing angle; the outline style cuts a notch in the border under it with a mask, so the field sits on any background. An error shakes the tag on a keyframe tween and a character budget fills as a thread under the field on glide. Tabbing into the field lifts and hangs the label exactly as a click does, and everything else, typing, selection, IME composition, paste and autofill, is the browser's own input.

KQ-1127 · HANG/LABEL

Shown on your reviews and orders.

Up to 32 characters.

display name · 0 of 32

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/hang-label

Props

label
string
default —
What the field is for: the visible label (the hanging tag is the real label element) and the input's accessible name.
value / defaultValue
string
default ""
The text. Controlled or uncontrolled.
onValueChange
(value: string) => void
default —
Fires with the new text on every edit: typing, paste, composition and autofill.
error
string | null
default —
What is wrong, in a sentence. Shown under the field in a polite live region, sets aria-invalid, and shakes the label when it arrives or changes.
shakeKey
string | number
default —
Change it to shake the label again with the same error, e.g. a form's submit count.
hint
string
default —
Helper text under the field, replaced by the error while one shows.
maxLength
number
default —
The character budget, also the input's native maxlength.
swing
number
default 5
How far the tag swings when it lands on the edge, in degrees, 0 to 10. A long label swings less, so its rising corner never leaves the component's box.
stiffness
number
default 220
The pendulum's spring stiffness, 60 to 600: low is a slow, lazy sway, high a quick, tight wobble.
style
"outline" | "filled" | "line"
default "outline"
A framed field with a notch cut under the tag, a filled field whose tag reads as a raised tab, or an underline.
budget
boolean
default true
Show the budget as a thread filling under the field, cobalt, then warn in the last 20%, with a count. Needs maxLength.
type
"text" | "email" | "tel" | "url" | "password" | "search"
default "text"
The input's type.
name
string
default —
The input's name, for forms.
autoComplete
string
default —
The input's autocomplete hint. Autofill lifts the label, even before the page has had a key.
inputMode
"none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search"
default —
The on-screen keyboard to offer.
placeholder
string
default —
Shown inside the field only once the label has lifted out of the way.
required
boolean
default —
The input's native required.
onFocus
(event: React.FocusEvent<HTMLInputElement>) => void
default —
The input's focus event.
onBlur
(event: React.FocusEvent<HTMLInputElement>) => void
default —
The input's blur event, e.g. to validate on leaving the field.
ref
React.Ref<HTMLInputElement>
default —
The input element, e.g. to focus the field when a submit is refused.
sound
boolean
default false
Play a tick on the frame the tag lands (only while the field has focus), a shrug with the error shake, and a detent on the key that fills the budget. Nothing plays, and no audio context exists, unless this is on.
disabled
boolean
default false
Dims the field and disables the input; the label stays where the text puts it.
className
string
default —
Merged onto the root's classes.