Component · Core
Textarea
Overview
Multi-line free-text entry — a message, a comment, a description. Textarea follows the same shared form-field model as Input: same Label pairing, spacing, tokens, and validation/messaging rules. This page covers only where Textarea genuinely differs — height and resize.
Anatomy
Container (border, --radius-sm, background) → native <textarea> showing either placeholder
text or an entered value. Same container chrome as Input.
- Container — the border, radius, and background (the same chrome as Input).
- Native
<textarea>— where the placeholder or the entered text renders.
Live Example
Default + helper text
Drag the bottom-right corner to make more room.
Error
Add at least 20 characters.
Read-only
Disabled
Code Example
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
/**
* Live Textarea example — the one baseline size (vertical-only resize) and the
* documented states, wired exactly as Input's shared form-field model: Label
* bound with `htmlFor` / `id`, `--scale-2` (gap-2) to the control, `--scale-1-5`
* (gap-1.5) to a supporting message. Helper is `--muted-foreground`; an error
* message is `--foreground` with `aria-invalid="true"` + `aria-describedby`.
*/
export function TextareaShowcase() {
return (
<div className="flex max-w-md flex-col gap-8">
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Default + helper text
</h3>
<div className="flex flex-col gap-2">
<Label htmlFor="textarea-default">Message</Label>
<Textarea
id="textarea-default"
aria-describedby="textarea-default-help"
placeholder="Tell us what happened…"
/>
<p
id="textarea-default-help"
className="text-xs leading-snug text-muted-foreground"
>
Drag the bottom-right corner to make more room.
</p>
</div>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Error
</h3>
<div className="flex flex-col gap-2">
<Label htmlFor="textarea-error">Description</Label>
<Textarea
id="textarea-error"
aria-invalid="true"
aria-describedby="textarea-error-text"
defaultValue="Too short"
/>
<p
id="textarea-error-text"
className="text-xs leading-snug text-foreground"
>
Add at least 20 characters.
</p>
</div>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Read-only
</h3>
<div className="flex flex-col gap-2">
<Label htmlFor="textarea-readonly">Original request</Label>
<Textarea
id="textarea-readonly"
readOnly
defaultValue={
"Submitted 3 days ago.\nThe export finished but the download link expired."
}
/>
</div>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Disabled
</h3>
<div className="flex flex-col gap-2">
<Label htmlFor="textarea-disabled" className="opacity-50">
Internal note
</Label>
<Textarea
id="textarea-disabled"
disabled
defaultValue="Editing is turned off for this record."
/>
</div>
</section>
</div>
);
}Variants
Sizes
One baseline size. Minimum height is --scale-20 (80px) — roughly three lines of "Body small"
text plus padding. It's a starting point the user can grow from by resizing, not a hard limit.
Resize behavior
Vertical-only resize (resize-y), by design — this is a Monogem decision, not just the browser
default left in place. Textarea allows vertical resizing so users can reveal more content without
breaking the horizontal layout. Horizontal resize is disabled (it breaks the surrounding layout);
resize is not disabled entirely (that would remove a real usability affordance for long content).
States
Same treatment as Input — Default, Focus, Disabled, Read-only, and Error all
use identical tokens and rules (including the flush focus-ring geometry and opacity-50 disabled
dimming), applied to a multi-line box. No dedicated hover state, same reasoning as Input.
"Filled" is a content condition here too, not a visual state — see
Input's note on entered content as a condition layered on top of a state, not a
state of its own.
Long content scrolls within the box (native overflow-y) once it exceeds the current resized
height — Textarea doesn't auto-grow to fit content in the baseline.
Usage Guidance
Tokens
Textarea reuses Input's token set plus one size step for minimum height; it introduces no component-specific styling values.
| Token | Where used |
|---|---|
--background | Default fill |
--muted | Read-only fill |
--foreground | Entered value, error message text |
--muted-foreground | Placeholder, helper text |
--destructive | Error border |
--input | Default border |
--ring | Focus ring |
--radius-sm | Corner radius |
--scale-3 | Padding |
--scale-20 | Minimum height |
Do / Don’t
Do
Don’t
- Enable horizontal resize — it breaks the surrounding layout.
- Remove resize entirely "to keep things tidy" — it's a real usability affordance for long content.
- Auto-grow the box to fit content in the baseline — that's a deferred, JS-driven behavior, not a CSS/native one.
Accessibility
- Same as Input — native
<label>association,aria-describedbyfor every visible supporting message,aria-invalid="true"only while actually invalid, nativedisabled/readonlyattributes.