Input React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React text input: icons and addons inside the box, a clear button, a loading spinner, a character counter and floating labels.
Click anywhere in the box, on an icon, on the padding or on a suffix like USD, and the caret lands in the text without jumping; the whole box takes a soft ring. Type and a clear button pops in at the end: tap it or press Escape and the text is gone while focus stays in the field. While loading a small spinner spins where the trailing icon was, and the field stays editable. With a floating label the label rests inside the box like a placeholder and glides up to the top edge on focus, when filled or when the browser autofills. With a counter the numbers turn amber at 90% and red at the limit. Enter submits the form like a native field. On a phone the text is 16px so iOS never zooms, the keypad follows the type (email, url, tel, decimal for amounts), the md box grows to 44px and the clear button has a 44px hit area.
Input is a free React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add input, together with UIProvider, Field, Spinner, Motion presets and useControllableState and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add inputUsage
Import Input from @/components/ui/input.
import { useRef, useState } from "react"
import { CircleCheck, MapPin } from "lucide-react"
import { Input } from "@/components/ui/input"
export function ListingDetails() {
const [title, setTitle] = useState("")
const [user, setUser] = useState("emma.carter")
const [checking, setChecking] = useState(false)
const timer = useRef<ReturnType<typeof setTimeout>>(undefined)
return (
<form className="flex flex-col gap-4">
<Input
name="title"
label="Listing title"
placeholder="e.g. Trek FX 2 city bike"
value={title}
onValueChange={setTitle}
error={title && title.length < 10 ? "The title needs at least 10 characters." : undefined}
showCount
maxLength={70}
required
enterKeyHint="next"
/>
<Input name="price" label="Price" prefix="$" inputMode="decimal" selectOnFocus defaultValue="1,250" />
<Input name="city" label="City" leftIcon={<MapPin />} clearable autoComplete="address-level2" />
<Input
label="Username"
value={user}
onValueChange={(next) => {
setUser(next)
// the check runs 1.4s after the last keystroke
setChecking(true)
clearTimeout(timer.current)
timer.current = setTimeout(() => setChecking(false), 1400)
}}
loading={checking}
rightIcon={<CircleCheck className="text-success-soft-foreground" />}
autoCapitalize="none"
enterKeyHint="done"
/>
</form>
)
}Props
labeldefault""ReactNodeRenders the field anatomy around the box: the label above it, linked with htmlFor. With floatingLabel it moves inside the box. Leave it empty for a bare input (then pass aria-label).
descriptiondefault""ReactNodeHelper text under the box, linked with aria-describedby. The error takes its place while it shows.
errordefault""ReactNodeReplaces the description with an alert icon and the text, slides in and marks the field invalid (red ring, aria-invalid). Empty means no error.
requireddefaultfalsebooleanThe red mark after the label (hidden from screen readers) and the native required attribute.
invaliddefaultfalsebooleanThe invalid look and aria-invalid without a message, for forms that show the error somewhere else.
disableddefaultfalsebooleanDims the field to 50% and blocks input; the native input is disabled, so the form does not submit it.
readOnlydefaultfalsebooleanThe value stays visible, selectable and submitted, but cannot change; the box drops its hover tint and the clear button. The demo fills in a sample value.
placeholderdefault""stringHint text while the field is empty. With a floating label it only shows while the label is floated (on focus, or always next to a prefix), never behind it.
defaultValuedefault""stringThe starting text while uncontrolled. Pass value and onValueChange instead to control it; a value set by a form library straight on the element shows up in the counter after the next focus.
prefixdefault""ReactNodeText before the input, like "https://" or "+40", in the muted addon look (long text truncates at 40% of the box), or an element rendered as is. A click on it lands in the input.
suffixdefault""ReactNodeText after the input, like "USD" or "@company.com", or an element such as a small Apply button. Buttons inside keep the input focused while pressed.
clearLabeldefault"Clear"stringThe clear button's accessible name and hover title. The demo passes Clear text (the City field always shows one).
loadingLabeldefault"Loading"stringWhat screen readers hear for the spinner while loading. The demo passes Checking.
namedefault""stringThe form field name. It lands on the native input, so FormData, autofill and {...register('titlu')} from react-hook-form work as on a plain input.
sizedefault"md""sm" | "md" | "lg"Height 32 / 40 / 48px with 13 / 14 / 15px text (16px on phones so iOS never zooms); md grows to 44px on touch. Floating labels make the box 44 / 52 / 60px.
variantdefault"soft""soft" | "outline" | "underline" | "ghost"soft is a filled well that works on every surface, outline a bordered box, underline a bottom line that grows a bar from the center on focus, ghost has no fill until hover (toolbars, table cells).
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corner shape, scaled with the size like the button's (md is 8 / 10 / 12px); full makes a pill. Ignored by underline.
accentdefault""stringAny CSS color for the focus ring, the outline border and underline bar on focus, and the caret. Empty uses the ring token (the brand orange).
floatingLabeldefaultfalsebooleanThe label sits inside the box like a placeholder and floats up, at 80%, on focus, when filled or when autofilled (CSS only, so it is right before hydration). The box grows to 44 / 52 / 60px for the room. Needs label; a prefix keeps it floated.
loadingdefaultfalsebooleanA small spinner at the end, in place of the right icon when there is one, and aria-busy. The input stays editable, e.g. while checking if a username is free.
typedefault"text"InputType: "text" | "email" | "url" | "tel" | "search" | "password" | "number"The native type, which also picks the phone keypad (email, url, tel, search). number renders a text field with a decimal keypad, so maxLength works and a scroll never changes it; number-input adds steppers and locale parsing, password-input the reveal button.
clearabledefaultfalsebooleanA clear button pops in while there is a value (never when disabled or read-only). It stays out of the Tab order; Escape clears from the keyboard. Focus stays in the input.
showCountdefaultfalsebooleanThe "12 / 70" counter at the end of the message row, amber from 90% and red at the limit. Needs maxLength.
maxLengthdefault0numberThe native character limit, also the counter's maximum. 0 means no limit.
selectOnFocusdefaultfalsebooleanSelects the whole text when the field takes focus, by click, tap or Tab, so typing replaces it. For codes and amounts.
valuestringThe text, controlled. Without onValueChange typing does nothing (like React); use readOnly for a fixed value.
onValueChange(value: string) => voidFires with the new text on every edit, including a clear.
onChange(event: ChangeEvent<HTMLInputElement>) => voidThe native change event, untouched. A clear fires it too, like a keystroke.
onClear() => voidCalled after the clear button or Escape emptied the field. Focus is already back in the input.
leftIconReactNodeA leading icon in the muted color; unsized svgs follow the size (14 / 16 / 18px). A click on it lands in the input.
rightIconReactNodeA trailing icon, e.g. a green check after a successful check. It pops in and out, and crossfades with the spinner while loading.
clearIconReactNodeReplaces the clear button's icon (a 16px X by default).
fieldPropsPartial<FieldProps>Anything else for the Field wrapper: optional and optionalLabel, labelAction, orientation, labelWidth, hideLabel, counterLabel, requiredIndicator.
idstringThe native input's id. Defaults to useId(); inside a <Field> the Field's id wins.
refRefObject<HTMLInputElement | null> or (node) => voidThe native input. Every other native prop lands there too: inputMode (defaults from the type), enterKeyHint, autoComplete, pattern, spellCheck, autoCapitalize, onBlur, onKeyDown, aria-* and data-*.
classNamesPartial<Record<"field" | "label" | "description" | "error" | "counter" | "box" | "input" | "prefix" | "suffix" | "leftIcon" | "rightIcon" | "clear" | "spinner" | "floatingLabel", string>>Extra classes for the inner parts. box targets the field box, input the native element.
classNamestringClasses for the outermost element: the Field root when there is a label, description, error or counter, else the box.
Examples
Icons and addons
A leading icon, text addons on both sides, and a small Apply button as the suffix: it checks the code for a second, keeps the input focused and swaps for the discount.
Variants and sizes
soft, outline, underline and ghost at 32, 40 and 48px, with icons, a clear button and a suffix.
Loading
An availability check 1.4s after typing stops: the check icon crossfades into a spinner and back; try emma for a taken name or a capital letter for the format error.
Clearable search
The clear button pops in with the first letter; Escape clears from the keyboard and focus never leaves the field. Every word matches on its own, accents folded; the label is kept for screen readers only (fieldProps.hideLabel).
Floating labels
An outline checkout form: labels rest inside the box and float up on focus or when filled; a prefix keeps the label up. Leave the email field to see its error.
Counter
showCount with maxLength 70: amber from 63 characters, red at the limit. Add the suggestions to walk it there.
States
Read-only (selectable, selected on focus), disabled with a prefix, and invalid without a message.
Accent and pill
radius full with a blue accent: focus the field and the ring and caret match the button next to it.
Files
- components/ui/input.tsx
FAQ
Is the React Input component free?
Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.
How do I install Input?
Run npx wingo-ui@latest add input. It also adds the Wingo UI items it builds on (UIProvider, Field, Spinner, Motion presets and useControllableState) and the npm packages motion and lucide-react. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/input.json.
How does Input behave on phones?
On a phone the text is 16px so iOS never zooms, the keypad follows the type (email, url, tel, decimal for amounts), the md box grows to 44px and the clear button has a 44px hit area.
See also
Used in
More React input components
Articles using this component
- Shadcn Number Input: Steppers, Min, Max and Decimals
Component guides8 min read
- Nextjs Contact Form That Sends Email and Blocks Spam
Build it: screens and apps10 min read
- React Settings Page: Section Nav, Save Bar, Danger Zone
Build it: screens and apps11 min read
- Claude Code shadcn Setup: MCP Server, Skill and CLAUDE.md
Building UI with AI agents12 min read