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.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | 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 | "" | The text. Controlled or uncontrolled. |
| onValueChange | (value: string) => void | — | Fires with the new text on every edit: typing, paste, composition and autofill. |
| error | string | null | — | 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 | — | Change it to shake the label again with the same error, e.g. a form's submit count. |
| hint | string | — | Helper text under the field, replaced by the error while one shows. |
| maxLength | number | — | The character budget, also the input's native maxlength. |
| swing | number | 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 | 220 | The pendulum's spring stiffness, 60 to 600: low is a slow, lazy sway, high a quick, tight wobble. |
| style | "outline" | "filled" | "line" | "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 | 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" | "text" | The input's type. |
| name | string | — | The input's name, for forms. |
| autoComplete | string | — | 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" | — | The on-screen keyboard to offer. |
| placeholder | string | — | Shown inside the field only once the label has lifted out of the way. |
| required | boolean | — | The input's native required. |
| onFocus | (event: React.FocusEvent<HTMLInputElement>) => void | — | The input's focus event. |
| onBlur | (event: React.FocusEvent<HTMLInputElement>) => void | — | The input's blur event, e.g. to validate on leaving the field. |
| ref | React.Ref<HTMLInputElement> | — | The input element, e.g. to focus the field when a submit is refused. |
| sound | boolean | 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 | false | Dims the field and disables the input; the label stays where the text puts it. |
| className | string | — | Merged onto the root's classes. |