Shadcn Textarea
Free shadcn/ui textarea component for React — auto-growing (field-sizing), character counter, resize control and validation examples. Copy or install in one command.
Installation
{
"registries": {
"@designrevision": "https://registry.designrevision.com/r/{name}.json"
}
}
Add to your existing components.json. Requires shadcn/ui v2.3+.
Packages
Props
No props documented yet.
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }
Examples
With label
The canonical multi-line field — a `Label` bound by `htmlFor`, with muted helper text below.
Packages
Props
No props documented yet.
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
export default function TextareaLabel01() {
return (
<div className="grid w-full max-w-sm gap-2">
<Label htmlFor="message">Your message</Label>
<Textarea id="message" placeholder="Type your message here…" />
<p className="text-sm text-muted-foreground">
We typically reply within a business day.
</p>
</div>
)
}
Autosize
Grows with its content up to a `max-h` and then scrolls — powered by the CSS `field-sizing-content` baked into the primitive, with no autosize library.
Packages
Props
No props documented yet.
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
export default function TextareaAutosize01() {
return (
<div className="grid w-full max-w-sm gap-2">
<Label htmlFor="bio">Bio</Label>
<Textarea
id="bio"
placeholder="Start typing — the field grows with your text…"
defaultValue="The height tracks the content via field-sizing-content. It grows as you type, stops at the max height, then scrolls."
className="max-h-40 resize-none"
/>
<p className="text-sm text-muted-foreground">
Grows with content up to a max height — no JavaScript.
</p>
</div>
)
}
Character counter
A live `current/max` count in `tabular-nums`, backed by the textarea’s `maxLength`.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
export default function TextareaCounter01() {
const maxLength = 180
const [value, setValue] = React.useState("")
return (
<div className="grid w-full max-w-sm gap-2">
<Label htmlFor="about">About you</Label>
<Textarea
id="about"
value={value}
maxLength={maxLength}
onChange={(e) => setValue(e.target.value)}
placeholder="A short bio…"
/>
<p className="text-right text-sm tabular-nums text-muted-foreground">
{value.length}/{maxLength}
</p>
</div>
)
}
With button
A comment field paired with a submit `Button` — the shape behind comment boxes and reply forms.
Packages
Props
No props documented yet.
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
export default function TextareaButton01() {
return (
<form className="grid w-full max-w-sm gap-2">
<Label htmlFor="comment">Add a comment</Label>
<Textarea id="comment" placeholder="Share your thoughts…" />
<div className="flex justify-end">
<Button type="submit">Comment</Button>
</div>
</form>
)
}
States
Invalid (`aria-invalid` switches on the destructive ring and border, paired with `aria-describedby`), disabled, and read-only fields.
Packages
Props
No props documented yet.
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
export default function TextareaStates01() {
return (
<div className="grid w-full max-w-sm gap-6">
<div className="grid gap-2">
<Label htmlFor="message-invalid">Message</Label>
<Textarea
id="message-invalid"
defaultValue="Too short"
aria-invalid="true"
aria-describedby="message-invalid-error"
/>
<p id="message-invalid-error" className="text-sm text-destructive">
Message must be at least 20 characters.
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="message-disabled">Disabled</Label>
<Textarea id="message-disabled" placeholder="Unavailable…" disabled />
</div>
<div className="grid gap-2">
<Label htmlFor="message-readonly">Read only</Label>
<Textarea
id="message-readonly"
readOnly
defaultValue="This content can be selected and copied, but not edited."
/>
</div>
</div>
)
}
Resize control
Constrain the drag handle with `resize-y` for vertical-only resizing, or `resize-none` to remove it entirely.
Packages
Props
No props documented yet.
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
export default function TextareaResize01() {
return (
<div className="grid w-full max-w-sm gap-6">
<div className="grid gap-2">
<Label htmlFor="ta-resize-y">Vertical resize handle</Label>
<Textarea
id="ta-resize-y"
className="resize-y"
placeholder="Drag the bottom-right corner to resize…"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="ta-resize-none">No resize handle</Label>
<Textarea
id="ta-resize-none"
className="resize-none"
placeholder="Manual resizing disabled…"
/>
</div>
</div>
)
}
The Shadcn Textarea is the multi-line counterpart to the Input — the canonical shadcn/ui textarea, a native <textarea> themed entirely with CSS variables. It ships one quietly powerful default: the box auto-grows with its content via the CSS field-sizing-content property, so it behaves like a polished chat or comment input out of the box, with no autosize library. Pair it with a Label and a Button; the examples below cover the patterns you reach for most.
When to use
Use a textarea for longer, free-form, multi-line text — a message, a comment, a bio, a description. For a single line (email, name, search) use an Input; for a fixed set of choices use a select or radio group. Match the field size to the expected answer: a one-sentence summary and a full support message should not look the same.
Autosize and sizing
The primitive includes field-sizing-content, so the height tracks the text as the user types — no useEffect, no react-textarea-autosize. Set min-h-* for the resting height and max-h-* to cap the growth (past the cap it scrolls). This is the modern, dependency-free replacement for JavaScript autosize, and it degrades gracefully to a normal rows-sized box where field-sizing is unsupported.
Resize control
By default the browser shows a drag handle. Add resize-y to allow vertical resizing only, or resize-none to remove the handle entirely — handy when the height is driven by autosize and you do not want users fighting it. The Resize control example shows both side by side.
Validation and states
Error state is driven by the standard aria-invalid attribute — set it and the textarea switches on the destructive border and ring automatically, no custom prop. This keeps it compatible with native validation and form libraries like React Hook Form. Always pair an invalid field with a visible message linked by aria-describedby. Disabled and read-only use the native attributes, so they behave and announce correctly.
Accessibility
Every textarea is keyboard-navigable with a visible 3px focus ring. Give each one a bound Label (or an aria-label when the design has no visible label). When you cap length with maxLength, surface the limit visibly — a counter — so the constraint is not a surprise on submit.
Theming
The textarea reads your design tokens (--input, --ring, --destructive, --muted-foreground), so it follows your theme — including dark mode — with zero overrides. Any palette produced by a shadcn-compatible theme generator applies automatically.