Skip to content
Drivn
7 min read

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.

1import { Input } from "@/components/ui/input"
2
3export default function Page() {
4 return (
5 <Input
6 type="text"
7 name="fullName"
8 placeholder="Enter your name"
9 />
10 )
11}

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.

1import { Input } from "@/components/ui/input"
2import { Label } from "@/components/ui/label"
3import { useId } from "react"
4
5export 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&apos;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.

1import { Input } from "@/components/ui/input"
2import { Label } from "@/components/ui/label"
3import { useId, useState } from "react"
4
5export 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.

1import { Input } from "@/components/ui/input"
2import { Search } from "lucide-react"
3
4export 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"
2import { Input } from "@/components/ui/input"
3import { Label } from "@/components/ui/label"
4import { Button } from "@/components/ui/button"
5import { useForm } from "react-hook-form"
6import { zodResolver } from "@hookform/resolvers/zod"
7import { z } from "zod"
8
9const schema = z.object({
10 email: z.string().email("Enter a valid email"),
11 password: z.string().min(8, "At least 8 characters"),
12})
13
14type FormValues = z.infer<typeof schema>
15
16export 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}
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

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.