How to Add an Input to a React App
Step-by-step guide to adding a copy-and-own text input to any React project with the Drivn CLI — forwardRef, native attributes, zero runtime deps.
A text input is the most common interactive element in any React app — a login field, a search box, a settings form. Building one from scratch is easy; building one that themes cleanly, forwards a ref to your form library, and stays consistent across every screen is the part teams get wrong. Most reach for a component library and inherit its runtime, its styling opinions, and its upgrade treadmill for a single <input>.
Drivn writes the component into your repository instead. The CLI copies an input.tsx file that is a React.forwardRef around a native <input>, with one styles.base string holding every class. It has no component dependencies, no icon imports, and — unusually for a UI component — no 'use client' directive, because it holds no state. Every native attribute you know (type, placeholder, value, onChange, required, disabled) passes straight through {...props} to the DOM element, and the forwarded ref means form libraries like react-hook-form read and focus it directly. After install the file is yours to read and restyle.
This guide adds the Input to an existing React app in about ten minutes — install the CLI, render an uncontrolled field, control it with useState, wire it to a form library, and restyle the one styles.base object. It works the same in Vite + React or a Next.js App Router project. For the Next.js-specific walkthrough see the Next.js Input guide.
Prerequisites
Before installing the Input, confirm your React project has the three things Drivn assumes: Tailwind CSS v4 installed and processing your CSS, TypeScript configured (the component ships as a .tsx file), and a @/ path alias pointing at your source directory. If you scaffolded with create-next-app, npm create vite, or npx drivn@latest create, all three are already wired. For a custom setup, check the compilerOptions.paths entry in tsconfig.json; the installation page lists the minimal config. Unlike compound components such as the dropdown or dialog, the Input pulls in no other Drivn components and no lucide-react icons — it is a single self-contained file, which makes it the simplest component to add first when you are trying Drivn in an existing app.
Step 1 — Install Drivn via the CLI
Run the CLI from your project root to add the Input source file. The command prompts once for your install directory (defaulting to src/components/ui/), writes input.tsx, and finishes — there are no dependencies to resolve, so nothing else is written to your project and nothing is added to package.json. No global config file is created; the result is one TypeScript file you edit like any other component. Confirm it landed in your editor, then commit it for a clean baseline. If your project uses a monorepo layout or a non-standard source path, the CLI docs cover the flags for targeting a custom location during install.
1 # add the Input — a single self-contained file 2 npx drivn add input
Step 2 — Render a basic input
Open the page where the field should live and import Input from your UI directory. In its simplest form it is a drop-in replacement for a native <input>: <Input type="email" placeholder="Email address" />. Because the component spreads {...props} onto the underlying element, every attribute you would set on a raw input works unchanged — type, name, placeholder, required, defaultValue, autoComplete, readOnly. Set type to match the field (email, password, search, number) and the browser handles the on-screen keyboard and native validation for free. An uncontrolled field inside a <form> needs no state and no handlers in your code at all — read the values from FormData on submit. See the Input docs for the full prop table, all of which are the native input attributes.
1 import { Input } from '@/components/ui/input' 2 3 export function SignupForm() { 4 return ( 5 <form className="space-y-4"> 6 <Input type="text" name="name" placeholder="Your name" /> 7 <Input type="email" name="email" placeholder="Email address" required /> 8 <Input type="password" name="password" placeholder="Password" required /> 9 </form> 10 ) 11 }
Step 3 — Control the value with useState
When the field drives live UI — a search box filtering a list, a form validating on each keystroke — control it with value and onChange. The Input passes both through {...props} to the native element, so it behaves exactly like a raw <input> you already control. Hold the value in useState, pass it as value, and update it from e.target.value in onChange. In a Next.js App Router project this is the point where the component needs a client boundary: mark the file 'use client' because useState runs in the browser, and keep it small so only the interactive field hydrates. The example below is a controlled search box; the same shape covers an email field that validates as you type or a slug field that lowercases input on change.
1 'use client' 2 import { useState } from 'react' 3 import { Input } from '@/components/ui/input' 4 5 export function SearchBox() { 6 const [query, setQuery] = useState('') 7 return ( 8 <Input 9 type="search" 10 placeholder="Search..." 11 value={query} 12 onChange={(e) => setQuery(e.target.value)} 13 /> 14 ) 15 }
Step 4 — Wire to react-hook-form and customize styles
For real forms, react-hook-form is the common pairing, and the Input needs no adapter for it. Because the component is a React.forwardRef, spreading register('email') onto it hands the library the ref, change, and blur handlers it needs to read and focus the field — no Controller wrapper required for the native case. Styling is just as direct: every class lives in one styles.base string at the top of the file you own. It sets the box (w-full h-10 px-4 border border-input rounded-[10px]), the text and placeholder colors, a focus:outline-none with a color transition, and the disabled treatment. Edit that one object and every input updates at once; pass a className like h-12 for a one-off, since it merges through cn(). Because the colors read from Tailwind tokens, changing them in theming re-themes every field for dark and light mode.
1 'use client' 2 import { useForm } from 'react-hook-form' 3 import { Input } from '@/components/ui/input' 4 5 export function EmailForm() { 6 const { register, handleSubmit } = useForm<{ email: string }>() 7 return ( 8 <form onSubmit={handleSubmit((data) => console.log(data))}> 9 <Input type="email" placeholder="you@example.com" {...register('email')} /> 10 </form> 11 ) 12 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
Frequently asked questions
Yes. Set the project up with TypeScript and Tailwind v4 first, then run npx drivn add input. The Input has no dependency on Next.js or any router — it renders anywhere React and Tailwind reach the DOM. It is also the simplest Drivn component to add because it pulls in no other components and no icon package; the CLI writes a single input.tsx file and nothing else, so there is nothing extra to configure after install.
Yes, without an adapter for the common case. The component is a React.forwardRef, so it forwards its ref to the underlying <input>. Spread register('fieldName') onto the Input and react-hook-form gets the ref, change, and blur handlers it needs to read and focus the field. You only need the Controller wrapper for fully controlled third-party inputs, not for this native-backed one. Keep the form in a client component marked 'use client'.
The Input source itself has no 'use client' directive, so an uncontrolled field renders fine inside a Server Component or a plain React tree with no client boundary. You add 'use client' to the component you write around it — not to the Input — only when you control the value with useState or wire it to a form library, because those hooks run in the browser. The field stays a native input either way.
All of them live in the single styles.base string at the top of input.tsx. The height is h-10, the radius is rounded-[10px], and the colors read from Tailwind tokens (border-input, text-foreground, text-muted-foreground). Edit that one object and every input updates at once. For a one-off change, pass a className like h-12 or rounded-full; it merges through cn() so your override wins over the base.
No — by design the Input ships a clean baseline with no aria-invalid border, no ring, and no file-input styling. That keeps the default surface minimal so it fits any design. To add an error state, edit styles.base to react to aria-invalid or pass a conditional className on the field when your validation fails. Because you own the file, the error treatment is a small local edit rather than a prop you have to override.

