Textarea
A multi-line text field that grows with its content, for messages, descriptions and feedback.
import { Field, FieldLabel } from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"
export default function TextareaDemo() {
return (
<Field className="w-80">
<FieldLabel htmlFor="textarea-demo">What could we do better?</FieldLabel>
<Textarea
id="textarea-demo"
placeholder="The export button is hard to find on mobile…"
/>
</Field>
)
}Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/textarea.jsonUsage
import { Field, FieldLabel } from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"<Field>
<FieldLabel htmlFor="message">Message</FieldLabel>
<Textarea id="message" placeholder="Tell us what happened…" />
</Field>Textarea shares its border, fill, focus halo and invalid styling with Input, so the two sit together in any form without special casing.
Examples
Grows with content
Textarea uses field-sizing: content: it starts at a comfortable three lines and grows as people type, so nobody writes into a keyhole. Cap the growth with a max-h-* class — past it, the field scrolls. In browsers without field-sizing support it keeps its minimum height and can be resized by dragging.
Grows with its content up to 240px, then scrolls.
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"
export default function TextareaAutoResize() {
return (
<Field className="w-full max-w-md">
<FieldLabel htmlFor="textarea-auto">Release notes</FieldLabel>
<Textarea
id="textarea-auto"
className="max-h-60"
defaultValue={
"Faster exports: CSV and XLSX files are generated in the background.\nNew: keyboard shortcuts for every table action.\nFixed: date filters now respect your workspace time zone."
}
/>
<FieldDescription>
Grows with its content up to 240px, then scrolls.
</FieldDescription>
</Field>
)
}Character limit
Show the limit before people hit it, count as they type, and switch to an error when they go over — rather than silently truncating their words. The counter is linked with aria-describedby; a separate polite live region speaks up only near the limit, so screen reader users aren't interrupted on every keystroke.
Shown on your public profile.
91/160
"use client"
import * as React from "react"
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"
const LIMIT = 160
export default function TextareaCharacterLimit() {
const [bio, setBio] = React.useState(
"Design engineer at prfct. I care about type, motion and interfaces that get out of the way."
)
const remaining = LIMIT - bio.length
const over = remaining < 0
return (
<Field data-invalid={over || undefined} className="w-full max-w-md">
<FieldLabel htmlFor="textarea-limit">Bio</FieldLabel>
<Textarea
id="textarea-limit"
value={bio}
onChange={(event) => setBio(event.target.value)}
aria-invalid={over || undefined}
aria-describedby="textarea-limit-count"
/>
{/* Announce only near the limit — not on every keystroke. */}
<span className="sr-only" aria-live="polite">
{remaining <= 20
? over
? `Over the limit by ${-remaining} characters`
: `Characters left: ${remaining}`
: ""}
</span>
<div className="flex items-start justify-between gap-4">
{over ? (
<FieldError>{`Keep your bio under ${LIMIT} characters.`}</FieldError>
) : (
<FieldDescription>Shown on your public profile.</FieldDescription>
)}
<FieldDescription
id="textarea-limit-count"
className={
over
? "shrink-0 text-danger-11 tabular-nums"
: "shrink-0 tabular-nums"
}
>
{bio.length}/{LIMIT}
</FieldDescription>
</div>
</Field>
)
}Disabled and read-only
Disabled textareas are skipped by focus and excluded from form data. Read-only ones stay focusable, selectable and scrollable — use them for text people need to read or copy, like an audit message.
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"
export default function TextareaStates() {
return (
<FieldGroup className="grid w-full max-w-xl gap-4 sm:grid-cols-2">
<Field data-disabled>
<FieldLabel htmlFor="textarea-disabled">Internal notes</FieldLabel>
<Textarea
id="textarea-disabled"
disabled
defaultValue="Locked while the invoice is being processed."
/>
</Field>
<Field>
<FieldLabel htmlFor="textarea-readonly">Audit message</FieldLabel>
<Textarea
id="textarea-readonly"
readOnly
defaultValue="Plan changed from Team to Enterprise by ada@prfct.dev on Sep 12."
/>
</Field>
</FieldGroup>
)
}In a comment form
A textarea often carries its own small set of actions. Keep the primary action last and the field wide enough for a real sentence.
"use client"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
export default function TextareaCommentForm() {
return (
<form
className="flex w-full max-w-lg gap-3"
onSubmit={(event) => event.preventDefault()}
>
<Avatar size="lg">
<AvatarFallback>GH</AvatarFallback>
</Avatar>
<div className="flex flex-1 flex-col gap-2">
<Textarea
name="comment"
placeholder="Leave a comment…"
aria-label="Comment"
className="min-h-24"
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="ghost" size="sm">
Cancel
</Button>
<Button type="submit" size="sm">
Comment
</Button>
</div>
</div>
</form>
)
}Guidelines
When to use
- Open-ended answers longer than a line: feedback, descriptions, bios, messages.
- Content where line breaks matter, such as release notes or addresses.
When not to use
- Short answers — an Input signals "keep it brief" better.
- Rich formatting such as bold text, lists or mentions — use a dedicated editor.
- A chat composer with attachments and send buttons — compose an Input Group with
InputGroupTextarea.
Size for the answer you expect
The starting height sets expectations. A one-line-tall textarea suggests a short answer; three to five lines invite a considered one. Don't disable resizing unless the layout genuinely can't accommodate it — people with larger text settings rely on it.
Optional · up to 500 characters
Accessibility
Textarea is a native <textarea>: it announces as a multi-line edit field and supports every browser feature for free, including spellcheck, dictation and IME composition.
- Label it. Associate a visible
FieldLabelwithhtmlFor/id, or add anaria-label. - Describe limits. Put limits and formatting hints in a
FieldDescriptionlinked witharia-describedby, not in the placeholder. - Announce sparingly. Live-region counters that speak on every keystroke drown out what people are typing. Announce only when they approach or exceed the limit.
- Contrast and size. The border meets 3:1 against the page, and text is 16px on small screens to prevent iOS zoom.
| Key | Behavior |
|---|---|
Tab | Moves focus into the textarea. |
Enter | Inserts a line break. It never submits the form. |
CtrlEnter⌘Enter | A common convention for submitting from a textarea — wire it up yourself when it fits the flow. |
API reference
Textarea
Renders a native <textarea> element and accepts all of its attributes.