Skip to content
Drivn
5 min read

How to Add a Textarea to a React App

Step-by-step guide to add a textarea to any React app with the Drivn CLI — a copy-paste, forwardRef Textarea with controlled, counter, and form-field modes.

Adding a textarea to a React app sounds trivial — it is one HTML element — but the moment you want a consistent border, a focus ring that matches the rest of your form, a character counter, or react-hook-form support, you are writing and re-writing the same wrapper. Drivn skips that by giving you the finished component to copy into your project. The Textarea is a single React.forwardRef around a native <textarea>, with every class held in one styles.base object and nothing imported past React and the local cn() helper — no Radix, no cva, no runtime UI dependency.

Because it is a plain forwardRef with no state or effects, the Textarea carries no 'use client' directive, so it renders in a Server Component or a client one without a boundary of its own. Its props type is React.TextareaHTMLAttributes<HTMLTextAreaElement>, which means every native attribute — placeholder, rows, maxLength, value, onChange, disabled, name — passes straight through, and the forwarded ref lands on the DOM node so form libraries register it directly.

This guide adds the Textarea to any React app: install the CLI, render an uncontrolled field, control the value with a live character counter, and wire it into a labelled form. For the App Router specifics see the Next.js Textarea guide, and for more layouts the Textarea examples.

Prerequisites

Before installing the Textarea, confirm your React project has the two things Drivn assumes: Tailwind CSS v4 installed and processing your styles, and a @/ path alias pointing at your source directory — the component ships as a .tsx file, so TypeScript is part of the stack. If you scaffolded with create-next-app, npm create vite, or npx drivn@latest create, both are already wired; for a custom setup the installation page lists the minimal config. The Textarea has no third-party import past React and the local cn utility, and no other Drivn component as a dependency, so nothing else needs to be present first. Unlike a dialog or a tabs component, it holds no state and runs no effect, so its source has no 'use client' directive — you can render it in a Server Component or a client component without thinking about the boundary.

Step 1 — Install Drivn via the CLI

Run the CLI from your project root to add the Textarea source file. The command prompts once for your install directory (defaulting to src/components/ui/) and writes a single textarea.tsx — no other files, because the component's only dependency past React is the cn utility. No global config file is created; the result is TypeScript source 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. Because the file is yours after install, future Drivn releases will not overwrite your edits — the whole component, from the styles.base object to the forwardRef, is one short file you can read top to bottom.

1# add the Textarea source file
2npx drivn add textarea

Step 2 — Render your first textarea

The simplest use is an uncontrolled Textarea with a placeholder. It renders a full-width box with a min-h-[80px] starting height, a rounded border-input border, and a muted placeholder, and the reader can drag the bottom edge taller because the base styles include resize-y. You write no state and no handlers — the browser owns the value until you read it from the form on submit. Because the component spreads {...props} onto the underlying <textarea>, add rows={5} to set the initial visible height or name="message" so the value is included when the surrounding <form> submits. This is the right default for a contact message, a quick note, or a description field. See the full prop list on the Textarea reference.

1import { Textarea } from '@/components/ui/textarea'
2
3export default function Page() {
4 return (
5 <Textarea placeholder="Type your message..." />
6 )
7}

Step 3 — Control the value and count characters

To read or limit the value as the reader types, switch to controlled mode: hold the text in React.useState, pass value, and update it in onChange from e.target.value. Because the Textarea forwards every native attribute, add maxLength to cap the length and read value.length to render a live counter beneath the field. This wrapper owns state, so it opens with a 'use client' directive. It is the pattern for a tweet-style box, a review with a length limit, or a bio field where you show remaining characters — drop a Button below it to submit. The onChange handler receives the standard React change event, the same signature you would use with a raw <textarea>, so there is no adapter to learn.

1'use client'
2
3import * as React from 'react'
4import { Textarea } from '@/components/ui/textarea'
5
6export function BioField() {
7 const [value, setValue] = React.useState('')
8
9 return (
10 <div>
11 <Textarea
12 value={value}
13 onChange={(e) => setValue(e.target.value)}
14 maxLength={200}
15 placeholder="Write a short bio..."
16 />
17 <p className="mt-2 text-sm text-muted-foreground">
18 {value.length}/200
19 </p>
20 </div>
21 )
22}

Step 4 — Wire it into a labelled form

In a real form you want a label tied to the field for accessibility. Wrap a native <label> and the Textarea together, link them with a shared htmlFor/id, and give the textarea a name so its value is captured on submit — the Textarea spreads these attributes straight onto the element, so there is nothing special to wire. This is how it sits beside an Input in a contact or feedback form: matching widths, matching border tokens, one labelled field per row. And because the ref forwards to the DOM node, the same field works with react-hook-form — spread register("message") onto the Textarea and it registers exactly like a native control, with no Controller wrapper needed for the common case.

1import { Textarea } from '@/components/ui/textarea'
2
3export default function FeedbackForm() {
4 return (
5 <div className="grid gap-2">
6 <label htmlFor="feedback" className="text-sm font-medium">
7 Your feedback
8 </label>
9 <Textarea
10 id="feedback"
11 name="feedback"
12 rows={5}
13 placeholder="Tell us what you think..."
14 />
15 </div>
16 )
17}
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

Install the Drivn CLI and run npx drivn add textarea, which writes a single textarea.tsx into your components directory. Then import { Textarea } from "@/components/ui/textarea" and render it with a placeholder. It is a forwardRef wrapper around the native <textarea>, so you can leave it uncontrolled and read the value from the form on submit, or pass value and onChange for controlled mode.

No. The Textarea holds no state and runs no effect, so its source has no 'use client' directive and renders in a Server Component or a client component alike. You only add 'use client' to your own wrapper when that wrapper controls the value with React.useState — the character-counter pattern — or otherwise uses a hook. The field itself never needs a client boundary.

Control the field: hold the text in React.useState, pass it as value, and update it in onChange from e.target.value. Render value.length beside the field and pass maxLength to enforce the ceiling. Because Drivn types its props as React.TextareaHTMLAttributes, both value and maxLength reach the underlying element, so the counter and the hard limit stay in sync with no extra logic.

Yes. The Textarea is wrapped in React.forwardRef, so its ref forwards to the DOM node. Spread register("fieldName") from react-hook-form onto the Textarea and it registers like a native control — no Controller wrapper needed for the common case. The same forwarding lets you focus the field imperatively or read it through a ref you attach yourself.

The base styles set resize-y and a min-h-[80px] starting height, so the reader can drag the field taller but not wider. To change the default height for every textarea, edit the min-h value in the single styles.base object at the top of the file you own. For a one-off, pass a className like min-h-[160px]; it merges through cn() so your override wins. Set rows to size the initial visible line count.