Input

Free · MIT

Six 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.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/input.json

The 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

Use it for
  • 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.
Reach for something else
  • 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 needUse
Standard form fieldvariant="outline" labelMode="top" (defaults)
Compact forms where labels sit insidelabelMode="floating" (or "inline" for short labels like "To:")
Filter or search bar in a toolbarvariant="filled" or "ghost", labelMode="none", leading={<Search />}, clearable
Minimal settings or sign-in pagesvariant="underline" with animation="underline-grow"
Playful or retro brandsvariant="neo" with animation="lift"
Over photos or gradientsvariant="glass"
Validation feedbackstate="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

PropTypeDefault
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.
booleantrue
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).
number3
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.
booleantrue
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