Next.js Textarea for the App Router
Add a Next.js textarea that works in Server Components, forms, and Server Actions. Drivn ships one forwardRef file, zero runtime deps, no client boundary.
A textarea is where a Next.js app collects the long-form input a single-line field cannot hold — a support message, a code snippet, a product review, a comment thread. In the App Router it sits on the same boundary every form control does: an uncontrolled textarea inside a server-rendered <form> needs no client JavaScript at all, while a controlled one — counting characters, validating on keystroke, wiring to react-hook-form — does. Splitting those two cases cleanly is what keeps a form fast, because the field itself can render on the server while only the logic that reads it hydrates.
Drivn's Textarea is built for exactly that split. After install it lives in src/components/ui/textarea.tsx as a single React.forwardRef around a native <textarea>, with every class held in one styles.base object. It is not marked 'use client' — no hooks, no state, no effects — so you can render it directly inside a server component or a <form> that posts to a Server Action. Forward the ref to a form library when you need controlled behaviour, and the same file covers both modes. Its only import past React is the local cn() helper.
This guide installs Drivn in a Next.js 16 project, drops the Textarea into a server-rendered form wired to a Server Action, switches to a controlled field with a character counter inside a client component, and restyles the one styles.base object. For the full reference see the Textarea docs; for the shadcn/ui comparison see Drivn vs shadcn/ui Textarea.
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 textarea. The Textarea has no component dependencies and no icon imports, so the CLI writes a single textarea.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 2 npx drivn add textarea
Render in a Server Component
Unlike a dialog or a drawer, the Textarea 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 <textarea> that spreads every standard HTML textarea attribute through {...props} — name, placeholder, rows, required, defaultValue, and maxLength all pass straight to the DOM element. Set rows to size the initial height, and because the base class includes resize-y the reader can drag the field taller without any code from you. An uncontrolled textarea inside a <form> needs no hooks at all. See the installation guide for project setup.
1 // app/feedback/page.tsx — a Server Component, no 'use client' 2 import { Textarea } from '@/components/ui/textarea' 3 4 export default function FeedbackPage() { 5 return ( 6 <form className="space-y-4"> 7 <Textarea name="message" placeholder="Tell us what you think..." rows={5} /> 8 </form> 9 ) 10 }
Wire the textarea to a Server Action
Because the Textarea 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 the field a name, attach a server function to the form's action, and read the value off FormData on the server — no onChange, no useState, no client component. This is the fastest possible long-text 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 maxLength to lean on native browser validation before the request is even sent. For live character counts or per-keystroke validation, the next section switches the same Textarea into a controlled field.
1 // app/feedback/page.tsx 2 import { Textarea } from '@/components/ui/textarea' 3 4 async function submitFeedback(formData: FormData) { 5 'use server' 6 const message = formData.get('message') 7 // persist the message... 8 } 9 10 export default function Feedback() { 11 return ( 12 <form action={submitFeedback} className="space-y-3"> 13 <Textarea name="message" placeholder="Your feedback" required /> 14 <button type="submit">Send</button> 15 </form> 16 ) 17 }
Controlled textarea with a character counter
When the field drives live UI — a comment box counting characters, 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 Textarea passes both through {...props} to the native element, so it behaves exactly like a raw <textarea> you already know: onChange fires with a DOM event, so read the text off e.target.value. For form libraries the forwardRef matters — register('message') from react-hook-form spreads a ref and change handlers onto the field, and because the Textarea forwards its ref to the underlying element, the library reads and focuses it directly with no Controller wrapper for the common case. Mark the component 'use client' and keep it small so only the interactive part hydrates.
1 'use client' 2 import { useState } from 'react' 3 import { Textarea } from '@/components/ui/textarea' 4 5 export function CommentBox() { 6 const [value, setValue] = useState('') 7 return ( 8 <div className="space-y-1"> 9 <Textarea 10 placeholder="Write a comment..." 11 value={value} 12 onChange={(e) => setValue(e.target.value)} 13 maxLength={280} 14 /> 15 <p className="text-sm text-muted-foreground">{value.length}/280</p> 16 </div> 17 ) 18 }
Customize the one styles object
Every class the Textarea 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 min-h-[80px] px-4 py-3, border border-input rounded-[10px]), the text and placeholder colors, a focus:outline-none with a focus-visible:ring-[3px] ring, resize-y so the field grows vertically only, and the disabled treatment. Edit that one object and every textarea in your app updates at once, and because the colors read from your Tailwind tokens (border-input, text-foreground, ring-ring), 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="min-h-[160px]" to make one field taller without touching the base. The styles object below is copied verbatim from textarea.ts.
1 const styles = { 2 base: cn( 3 'w-full min-h-[80px] px-4 py-3', 4 'border border-input rounded-[10px]', 5 'text-foreground placeholder:text-muted-foreground text-sm', 6 'focus:outline-none transition-[color,box-shadow]', 7 'focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:border-ring', 8 'resize-y disabled:opacity-50 disabled:cursor-default' 9 ), 10 }
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
Yes — the Textarea 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 <textarea>. 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 Textarea a name, attach your server function to the form's action prop, and read the value from FormData on the server. Because the Textarea forwards every native attribute through {...props}, name, required, rows, and maxLength 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 maxLength to trigger native browser validation before the request is sent.
Control the field in a client component: hold the text in useState, pass it as value, and update it in onChange from e.target.value. Render the count from value.length beside the field, and pass maxLength so the browser enforces the ceiling. Because the Textarea spreads native attributes, both value and maxLength reach the underlying element, so the counter and the hard limit stay in sync with no extra logic.
The base class sets resize-y, so the reader can drag the field taller but not wider. The starting height is min-h-[80px] in the single styles.base string at the top of textarea.tsx — edit it there and every textarea updates at once. For a one-off, pass a className like min-h-[160px]; it merges through cn() so your override wins. Set rows on the element to size the initial visible line count instead.

