React Input Component Examples
Drop-in React Input examples: text, email, password, search with icon, labeled field, error state, and react-hook-form integration. Zero runtime deps.
A text input is the workhorse of every form in a shipped React app — the email field on a sign-in screen, the search bar at the top of a Sidebar, the password input on a settings page, the number field next to a quantity stepper. The Input component in Drivn is a 25-line wrapper around the native <input> element that forwards every standard input attribute through React.InputHTMLAttributes<HTMLInputElement> and applies one styles.base className with h-10, rounded-[10px], px-4, text-sm, and a disabled:opacity-50 disabled:cursor-default pair. Zero runtime UI dependencies, full ref forwarding through React.forwardRef.
This page collects the patterns that come up in shipped forms. Each example is copy-paste ready and uses the same @/components/ui/input import path the Drivn CLI installs under, so the snippets compile the moment you run npx drivn add input. Because the Input keeps its surface deliberately small, several patterns — leading icons, error states, label pairings — are wrappers around the input rather than props on it. That tradeoff is intentional: the field-wrapper pattern stays composable across Label, helper text, and inline validation messages without props sprawling on the input itself.
The examples below cover the common type variations (text, email, password, number, search), how to pair the Input with a Label and helper text, the error-state pattern using aria-invalid plus a destructive border, a search input with a leading Lucide icon, and a full react-hook-form field with zod validation. Every snippet is TypeScript — Drivn ships TypeScript-only.
Basic text input
The minimum viable Input is a type and a placeholder. Drop the component into a form, set the type to text (the default), and pass a placeholder string. Every other native attribute — name, id, required, autoComplete, value, defaultValue, onChange, onBlur — works without configuration because the prop type is React.InputHTMLAttributes<HTMLInputElement>. The default styling is a 40-pixel-tall field with a 10-pixel border radius, four-pixel horizontal padding, and a muted placeholder color that matches the rest of the Drivn surface tokens.
The component uses React.forwardRef so you can attach a ref through useRef for imperative focus control, or wire register from a form library like react-hook-form. For controlled inputs, pass value and onChange; for uncontrolled inputs, pass defaultValue and read from the ref or a form submit handler. The pattern matches every other native input wrapper, so existing form code drops in unchanged.
1 import { Input } from "@/components/ui/input" 2 3 export default function Page() { 4 return ( 5 <Input 6 type="text" 7 name="fullName" 8 placeholder="Enter your name" 9 /> 10 ) 11 }
Common input types: email, password, number, search
The native type attribute switches the keyboard and validation behavior of the input — email opens the email keyboard on iOS and triggers built-in format validation, password masks characters and opts the field out of autofill suggestions, number opens the numeric keyboard and accepts only numeric characters, search adds a clear button in some browsers. Drivn passes the type straight through to the native input, so every browser-native behavior works out of the box without extra props on the component.
Pair type="email" with autoComplete="email" and inputMode="email" for the best mobile keyboard. Pair type="password" with autoComplete="current-password" on sign-in and autoComplete="new-password" on sign-up. Pair type="number" with min, max, and step for guided numeric entry. The Input forwards every one of these attributes because they live on React.InputHTMLAttributes<HTMLInputElement>. See the Input docs for the full attribute list.
1 import { Input } from "@/components/ui/input" 2 3 export default function Page() { 4 return ( 5 <div className="space-y-3"> 6 <Input 7 type="email" 8 placeholder="you@example.com" 9 autoComplete="email" 10 /> 11 <Input 12 type="password" 13 placeholder="Password" 14 autoComplete="current-password" 15 /> 16 <Input 17 type="number" 18 placeholder="0" 19 min={0} 20 max={100} 21 step={1} 22 /> 23 <Input 24 type="search" 25 placeholder="Search components..." 26 /> 27 </div> 28 ) 29 }
Labeled field with helper text
Every production form pairs an input with a label and, usually, a helper paragraph. The pattern is <label> over <input> over <p> — the label provides the accessible name, the input captures the value, the helper paragraph explains the format or shows a validation error. Use Drivn's Label component for the label so the font and color match the rest of the form rhythm, set htmlFor to the input's id, and put the helper paragraph at text-xs text-muted-foreground for the muted style or text-destructive for an error.
The id on the input and the htmlFor on the label do the accessibility work — screen readers announce the label when the input is focused, and clicking the label focuses the input. For a generated id, use React's useId hook so the id stays stable across re-renders without colliding with other fields on the page.
1 import { Input } from "@/components/ui/input" 2 import { Label } from "@/components/ui/label" 3 import { useId } from "react" 4 5 export default function Page() { 6 const id = useId() 7 return ( 8 <div className="space-y-2"> 9 <Label htmlFor={id}>Email address</Label> 10 <Input 11 id={id} 12 type="email" 13 placeholder="you@example.com" 14 autoComplete="email" 15 /> 16 <p className="text-xs text-muted-foreground"> 17 We'll send a magic link to this address. 18 </p> 19 </div> 20 ) 21 }
Inline error state with aria-invalid
The Drivn Input has no built-in error variant — error visuals live at the field-wrapper level. The pattern is to set aria-invalid={true} on the input when an error string is present and add aria-[invalid=true]:border-destructive to styles.base after install (one-line edit), then render a destructive paragraph under the input with the message. The aria-invalid attribute is also the right accessibility primitive: screen readers announce the field as invalid, and assistive tech links the helper paragraph through aria-describedby if you set it.
The field stays composable: a Label on top, the Input with aria-invalid, an aria-describedby reference, and a destructive paragraph underneath. The same shape works whether the error string comes from a synchronous validator, a server response, or react-hook-form's formState.errors. See Drivn vs shadcn Input for how shadcn handles the same pattern with a built-in aria-invalid style.
1 import { Input } from "@/components/ui/input" 2 import { Label } from "@/components/ui/label" 3 import { useId, useState } from "react" 4 5 export default function Page() { 6 const id = useId() 7 const errorId = `${id}-error` 8 const [email, setEmail] = useState("") 9 const error = email && !email.includes("@") ? "Enter a valid email" : null 10 11 return ( 12 <div className="space-y-2"> 13 <Label htmlFor={id}>Email</Label> 14 <Input 15 id={id} 16 type="email" 17 value={email} 18 onChange={(e) => setEmail(e.target.value)} 19 aria-invalid={!!error} 20 aria-describedby={error ? errorId : undefined} 21 className={error ? "border-destructive" : undefined} 22 /> 23 {error && ( 24 <p id={errorId} className="text-xs text-destructive"> 25 {error} 26 </p> 27 )} 28 </div> 29 ) 30 }
Search input with a leading icon
A search bar usually shows a magnifying-glass icon on the left edge of the input. Drivn's Input has no leftIcon prop — the recommended pattern is a relative wrapper that absolutely positions a Lucide icon inside the input's padding area, then bumps the input's pl-10 so the text starts past the icon. The wrapper lives in your form code rather than on the Input itself, which keeps the Input file at its minimal size and lets you reuse the same wrapper for trailing icons, clear buttons, or character-count badges.
The icon goes inside the wrapper at absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none. The pointer-events-none rule matters: without it, clicking the icon swallows the click that would otherwise focus the input. For a clear button on the right, swap to right-2, drop the pointer-events-none, and wire an onClick that resets the input value. Pair this with the Command component when the search drives a results list.
1 import { Input } from "@/components/ui/input" 2 import { Search } from "lucide-react" 3 4 export default function Page() { 5 return ( 6 <div className="relative w-full max-w-sm"> 7 <Search 8 className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" 9 /> 10 <Input 11 type="search" 12 placeholder="Search components..." 13 className="pl-10" 14 /> 15 </div> 16 ) 17 }
react-hook-form + zod validation
For real forms, wire the Input to react-hook-form and zod. Define a zod schema with the field types and constraints, pass it through zodResolver into useForm, and spread register("fieldName") onto the Input. The register function returns name, onChange, onBlur, and ref props — every one of those works on Drivn's Input because it forwards ref through React.forwardRef and accepts every native input attribute.
The error message comes from formState.errors[fieldName]?.message. Pair it with the aria-invalid pattern from the previous section to get a fully accessible field with synchronous validation, ref forwarding, and a destructive helper paragraph in roughly twenty lines of JSX. The same pattern scales to multi-field forms — pair Input with Textarea, Select, and Checkbox and submit the whole form through handleSubmit.
1 "use client" 2 import { Input } from "@/components/ui/input" 3 import { Label } from "@/components/ui/label" 4 import { Button } from "@/components/ui/button" 5 import { useForm } from "react-hook-form" 6 import { zodResolver } from "@hookform/resolvers/zod" 7 import { z } from "zod" 8 9 const schema = z.object({ 10 email: z.string().email("Enter a valid email"), 11 password: z.string().min(8, "At least 8 characters"), 12 }) 13 14 type FormValues = z.infer<typeof schema> 15 16 export default function SignInForm() { 17 const { register, handleSubmit, formState: { errors } } = useForm<FormValues>({ 18 resolver: zodResolver(schema), 19 }) 20 21 return ( 22 <form onSubmit={handleSubmit((data) => console.log(data))} className="space-y-4"> 23 <div className="space-y-2"> 24 <Label htmlFor="email">Email</Label> 25 <Input 26 id="email" 27 type="email" 28 autoComplete="email" 29 aria-invalid={!!errors.email} 30 {...register("email")} 31 /> 32 {errors.email && ( 33 <p className="text-xs text-destructive">{errors.email.message}</p> 34 )} 35 </div> 36 <div className="space-y-2"> 37 <Label htmlFor="password">Password</Label> 38 <Input 39 id="password" 40 type="password" 41 autoComplete="current-password" 42 aria-invalid={!!errors.password} 43 {...register("password")} 44 /> 45 {errors.password && ( 46 <p className="text-xs text-destructive">{errors.password.message}</p> 47 )} 48 </div> 49 <Button type="submit">Sign in</Button> 50 </form> 51 ) 52 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
For a controlled input, pass value and onChange — the parent component holds the state in useState and the input mirrors it on every keystroke. For an uncontrolled input, pass defaultValue (optional) and read the current value either through a ref attached via useRef and inputRef.current.value, or from the FormData of the surrounding <form> on submit. Both patterns work because the Drivn Input forwards ref through React.forwardRef and passes every native attribute through React.InputHTMLAttributes<HTMLInputElement>.
Not by default — the Input registry source has no icon prop, intentionally. The component file is about 25 lines and keeps a single styles.base className. The recommended pattern is a relative wrapper around the Input with an absolutely positioned Lucide icon inside the padding area, plus pl-10 or pr-10 on the Input to push the text past the icon. If your project uses this pattern often, edit the local @/components/ui/input file after install to accept a leftIcon prop — the file lives in your repo, so the change is yours to make and own.
The default styles.base is focus:outline-none transition-colors — no ring. The decision is intentional: in the labeled-field pattern (Label + Input + helper paragraph), the visual focus is usually carried by the surrounding layout and form rhythm rather than a per-input ring, and a ring on every input across a dense form can feel busy. If you want a ring, add focus:ring-2 focus:ring-ring focus:ring-offset-2 to styles.base after install. See Drivn vs shadcn Input for how shadcn handles this differently with focus-visible:ring-[3px].
Yes. register("fieldName") from useForm returns name, onChange, onBlur, and ref, and the Drivn Input forwards every one of those because it uses React.forwardRef and accepts the full React.InputHTMLAttributes<HTMLInputElement> shape. Spread {...register("fieldName")} onto the Input and the form library wires controlled state, validation, and ref handling for you. Pair with zodResolver from @hookform/resolvers/zod for schema validation — the react-hook-form page walks through the full pattern.
Yes — the Input file has no "use client" directive because it uses no client-only APIs. There is no useState, no useEffect, no event handler attached inside the component. You can render the Input inside a Server Component to capture form data through a Server Action, or import it into a Client Component when you need controlled state or react-hook-form integration. Next.js draws the client boundary based on the consumer file, so the same Input file works in both rendering modes without configuration.

