Skip to content
Drivn
5 min read

Next.js Input for the App Router

Add a Next.js text input that works in Server Components, forms, and Server Actions. Drivn ships one forwardRef file, zero runtime deps, no client boundary.

A text input is the smallest interactive surface in most apps, and in the Next.js App Router it sits at an interesting boundary: an uncontrolled input needs no client JavaScript at all, but a controlled one — validating on keystroke, wiring to react-hook-form, reacting to a Server Action — does. Getting the split right keeps forms fast, because the field itself can render on the server while only the logic that reads it runs on the client.

Drivn's Input is built for exactly that split. After install it lives in src/components/ui/input.tsx as a single React.forwardRef around a native <input>, with every class held in one styles.base object. It is not marked 'use client' — there are no hooks, no effects, no state — so you can render it directly inside a server-rendered page or a <form> that posts to a Server Action. Forward the ref to a form library when you need controlled behavior, and the same file covers both modes.

This guide installs Drivn in a Next.js 16 project, drops the Input into a server-rendered form wired to a Server Action, switches to a controlled field inside a client component, and restyles the one styles.base object every input reads from. Every snippet comes from the component's real API. For the full reference see the Input docs; for the shadcn/ui comparison see Drivn vs shadcn/ui Input.

Install in a Next.js 16 project

Drivn installs through a small CLI that writes the component source directly into your repository — there is no runtime npm package to version-lock. From the root of your Next.js 16 project run npx drivn add input. The Input has no component dependencies and no icon imports, so the CLI writes a single input.tsx file and nothing else. The CLI reference documents every flag, including targeting a custom directory or installing several components at once. After install you own the file; future Drivn releases will not overwrite it. Commit the change for a clean baseline before you start wiring it into forms, and confirm the file landed under your UI directory in the editor.

1# from the root of your Next.js 16 project
2npx drivn add input

Render in a Server Component

Unlike a dropdown or a drawer, the Input holds no state and runs no effect, so its source has no 'use client' directive. That means you can import and render it inside a server component with zero client JavaScript shipped for the field itself. The component is a React.forwardRef around a native <input> that spreads every standard HTML input attribute through {...props}type, placeholder, name, required, defaultValue, and the rest all pass straight to the DOM element. Set type to match the field (email, password, search, number) and the browser handles validation and virtual keyboards for free. Because it is a plain server-renderable element, an uncontrolled input inside a <form> needs no hooks in your code at all. See the installation guide for project setup.

1// app/contact/page.tsx — a Server Component, no 'use client'
2import { Input } from '@/components/ui/input'
3
4export default function ContactPage() {
5 return (
6 <form className="space-y-4">
7 <Input type="text" name="name" placeholder="Your name" />
8 <Input type="email" name="email" placeholder="Email address" required />
9 </form>
10 )
11}

Wire the input to a Server Action

Because the Input renders on the server and forwards every native attribute, it drops straight into the App Router's Server Action form pattern with no client boundary. Give each field a name, attach a server function to the form's action, and read the values off FormData on the server — no onChange, no useState, no client component. This is the fastest possible form in Next.js: the browser posts the form natively, the action runs on the server, and the field never needed to hydrate. Add required or type="email" to lean on native browser validation before the request is even sent. For richer client-side validation, the next section switches the same Input into a controlled field.

1// app/subscribe/page.tsx
2import { Input } from '@/components/ui/input'
3
4async function subscribe(formData: FormData) {
5 'use server'
6 const email = formData.get('email')
7 // persist the email...
8}
9
10export default function Subscribe() {
11 return (
12 <form action={subscribe} className="flex gap-2">
13 <Input type="email" name="email" placeholder="you@example.com" required />
14 <button type="submit">Subscribe</button>
15 </form>
16 )
17}

Controlled input in a client component

When the field drives live UI — a search box filtering a list, a form with per-keystroke validation, or a react-hook-form integration — move it into a client component and 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 know. For form libraries, the forwardRef matters: register('email') from react-hook-form spreads a ref and change handlers onto the field, and because the Input forwards its ref to the underlying element, the library reads and focuses it directly. No adapter or Controller wrapper is required for the common case. Mark the component 'use client' and keep it small so only the interactive part hydrates.

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}

Customize the one styles object

Every class the Input renders lives in a single styles.base string at the top of the file you own after install — there are no size or variant maps to learn. 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 in your app updates at once, and because the colors read from your Tailwind tokens (border-input, text-foreground, text-muted-foreground), changing them in theming re-themes the field for dark and light mode automatically. The className prop merges through cn() for per-instance overrides — pass className="h-12" to make one field taller without touching the base. The styles.base slice below is copied verbatim from input.ts.

1const styles = {
2 base: cn(
3 'w-full h-10 px-4 border border-input rounded-[10px]',
4 'text-foreground placeholder:text-muted-foreground text-sm',
5 'focus:outline-none transition-colors',
6 'disabled:opacity-50 disabled:cursor-default'
7 ),
8}
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

Yes — the Input is one of the few Drivn components that is not a client component. Its source has no 'use client' directive because it holds no state and runs no effect; it is a React.forwardRef around a native <input>. You can render it directly inside a server-rendered page or a <form> that posts to a Server Action, and an uncontrolled field ships zero client JavaScript. You only need a client boundary when you control the value with useState or wire it to a form library.

Give the Input a name, attach your server function to the form's action prop, and read the value from FormData on the server. Because the Input forwards every native attribute through {...props}, name, required, and type all reach the DOM element. The form posts natively and the action runs on the server, so the field never has to hydrate. Add required or type="email" to trigger native browser validation before the request is sent.

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'.

Both live in the single styles.base string at the top of input.tsx. The height is h-10 and the radius is rounded-[10px] — edit them there 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. Because the colors read from Tailwind tokens, changing them in theming re-themes every field for dark and light mode.