Skip to content
Drivn
5 min read

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
2npx 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.

1import { Input } from '@/components/ui/input'
2
3export 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'
2import { useState } from 'react'
3import { Input } from '@/components/ui/input'
4
5export 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'
2import { useForm } from 'react-hook-form'
3import { Input } from '@/components/ui/input'
4
5export 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}
Get started

Install Drivn in one command

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

Follow Drivn updates
New components, improvements, and guides every release.
Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

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.