Input
Free · MITSix field styles with top, floating, inline or hidden labels, focus ring, glow, underline-grow or lift, error shake with sliding messages, icon and text addons, clear button, counter and auto-growing textarea.
Installation
$ npx shadcn@latest add https://goodui.dev/r/input.jsonThe file lands in components/goodui/input.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/input works too.
Usage
import * as React from "react"
import { Mail } from "lucide-react"
import { Input } from "@/components/goodui/input"
export function InviteField() {
const [email, setEmail] = React.useState("")
const [error, setError] = React.useState("")
const [attempt, setAttempt] = React.useState(0)
const submit = (e: React.FormEvent) => {
e.preventDefault()
setAttempt((n) => n + 1)
setError(email.includes("@") ? "" : "Enter a valid work email.")
}
return (
<form onSubmit={submit} className="flex max-w-sm flex-col gap-3">
<Input
type="email"
label="Invite a teammate"
description="They get an email with a link to join your studio."
placeholder="name@studio.com"
leading={<Mail />}
clearable
value={email}
onValueChange={setEmail}
state={error ? "error" : "default"}
message={error}
shakeKey={attempt}
/>
<button type="submit" className="self-start text-sm font-medium underline">
Send invite
</button>
</form>
)
}When to use
- Any single-line text field in a form: name, email, price, URL, search.
- Validated forms that need an error or success message under the field, with a shake on a failed submit.
- Fields with units or prefixes: https:// before a domain, $ before a price, .com after a subdomain.
- Notes and messages that should grow with the text: multiline with autoResize.
- Picking from a fixed list of options: use select.
- A one-time code: use otp-input. Several values as chips: use tag-input.
- A search box that expands from an icon in a toolbar: use search-expand.
| If you need | Use |
|---|---|
| Standard form field | variant="outline" labelMode="top" (defaults) |
| Compact forms where labels sit inside | labelMode="floating" (or "inline" for short labels like "To:") |
| Filter or search bar in a toolbar | variant="filled" or "ghost", labelMode="none", leading={<Search />}, clearable |
| Minimal settings or sign-in pages | variant="underline" with animation="underline-grow" |
| Playful or retro brands | variant="neo" with animation="lift" |
| Over photos or gradients | variant="glass" |
| Validation feedback | state="error" | "success" with message; shakeKey to shake again on each submit |
Examples
Floating label
<Input labelMode="floating" label="Business name" defaultValue="Glow Studio" />Addons and prefix
<Input label="Booking page" addonBefore="https://" addonAfter=".goodui.app" placeholder="glow-studio" />
<Input label="Deposit" leading="$" trailing="USD" inputMode="decimal" placeholder="50.00" />Growing textarea with counter
<Input multiline autoResize rows={2} label="Note for the client" maxLength={280} showCount placeholder="Please arrive 10 minutes early." />API reference
| Prop | Type | Default |
|---|---|---|
| variant Field style: outline (default), filled, underline, ghost, neo-brutalist or frosted glass. | "outline" | "filled" | "underline" | "ghost" | "neo" | "glass" | "outline" |
| tone Focus color (ring, border, floating label). Overridden by state="error" or "success". | "primary" | "neutral" | "success" | "warning" | "danger" | "info" | "primary" |
| size | "sm" | "md" | "lg" | "md" |
| shape | "square" | "rounded" | "pill" | "rounded" |
| shadow Field shadow; inner draws an inset shadow. Ignored by neo, which has its own offset shadow. | "none" | "sm" | "md" | "inner" | "none" |
| animation How the field reacts to focus. | "none" | "ring" | "glow" | "underline-grow" | "lift" | "ring" |
| label Field label; placement is set by labelMode. | React.ReactNode | — |
| labelMode top: above the field. floating: inside, floats up on focus. inline: inside, before the value. none: screen-reader only. | "top" | "floating" | "inline" | "none" | "top" |
| description Helper text under the field (hidden while a message shows). | React.ReactNode | — |
| state Validation state: colors the field, adds an icon, sets aria-invalid on error and shows message. | "default" | "error" | "success" | "default" |
| message Error or success text; slides in under the field. | React.ReactNode | — |
| shake Shake the field when it enters the error state. | boolean | true |
| shakeKey Change it (e.g. a submit counter) to shake again while already in the error state. | React.Key | — |
| leading Icon or short text inside the field, before the value. | React.ReactNode | — |
| trailing Icon or short text inside the field, after the value. | React.ReactNode | — |
| addonBefore Attached segment before the field, e.g. "https://". | React.ReactNode | — |
| addonAfter Attached segment after the field, e.g. ".com". | React.ReactNode | — |
| clearable Shows a × button while the field has a value. | boolean | — |
| showCount Shows "12/80" when maxLength is set. | boolean | — |
| multiline Renders a <textarea>. | boolean | — |
| rows Visible rows of the textarea (multiline only). | number | 3 |
| autoResize Textarea grows with its content. | boolean | — |
| value Controlled value. | string | — |
| defaultValue Initial value when uncontrolled. | string | — |
| onValueChange Called with the new string on typing and when the clear button empties the field. | (value: string) => void | — |
| onChange Native change event. The clear button fires it too (with an empty value), so form libraries see the reset. | (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void | — |
| fullWidth false shrinks the field to its content width. | boolean | true |
| fieldClassName Class for the field box (the root gets className). | string | — |
| labels Text for built-in controls and screen readers; pass translations here. | Partial<InputLabels> | — |
Also accepts every <input> attribute.
Accessibility
- Renders a real <input> or <textarea> with a <label htmlFor>; the id is generated with useId unless you pass one.
- labelMode="none" keeps the label for screen readers: a string label becomes aria-label, any other node becomes an sr-only <label>.
- state="error" sets aria-invalid. The description or message is linked with aria-describedby and sits in an aria-live="polite" region, so new errors are announced.
- The clear button has aria-label "Clear" (labels.clear) and returns focus to the field.
- Error and success also show an icon, so the state does not rely on color alone.
Motion
- Focus: ring adds a 3px tone-colored ring in 200 ms (default), glow adds a soft bloom in 300 ms, lift raises the field 2px, underline-grow draws a bar from the center in 300 ms.
- Floating label: moves up and scales to 0.78 in 180 ms when the field is focused or has a value.
- Message: slides down and expands its height on a spring (stiffness 500, damping 36); the description cross-fades in 150 ms.
- Shake: a 420 ms horizontal shake when the field enters the error state, when the error text changes, or when shakeKey changes.
- Reduced motion: no shake, no lift and no CSS transitions; messages appear and disappear instantly.
Notes for AI
Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.
- Use value + onValueChange (string) for controlled fields; onChange still receives the native event. The clear button fires a real input event, so both onChange and onValueChange get "".
- ref reaches the <input> (or <textarea>), and uncontrolled fields stay native (defaultValue), so react-hook-form's register() works directly.
- Validation is yours: set state="error" and message. Increment shakeKey on every failed submit so the field shakes even if the message did not change.
- tone only changes the focus color. state="error" or "success" overrides it, so do not set tone="danger" for errors.
- className goes on the outer wrapper (label, field and message); use fieldClassName to style the field box itself.
- showCount needs maxLength. autoResize and rows only apply with multiline. labels={{ clear: "Löschen" }} translates the clear button.
FAQ
How do I use it with react-hook-form?
Spread register("email") onto it: ref, name, onChange and onBlur reach the native field, and the clear button reports the empty value. Or use a Controller with value and onValueChange. Pass fieldState.error?.message to message with state="error".
Can I show the error only after submit?
Yes. The component never validates on its own; it shows exactly the state and message you pass, whenever you pass them.
Related
Fully custom single or multi select: outline, filled, ghost, underline, pill and neo triggers; icons, avatars, descriptions and groups; search with match highlight, chips, clear, type-ahead and full keyboard; animated menu that flips up when space runs out.
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.
One-time code input from plain inputs with auto-advance, paste and SMS autofill, a blinking caret and popping digits, then a pulsing verify, green success bounce or red shake.
Chips input where Enter, comma or paste adds tags that spring in, Backspace arms then removes, duplicates shake, with a filtered suggestions dropdown, validation and a max count.