Skip to content
Monogem

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

textarea-showcase.tsx
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.

TokenWhere used
--backgroundDefault fill
--mutedRead-only fill
--foregroundEntered value, error message text
--muted-foregroundPlaceholder, helper text
--destructiveError border
--inputDefault border
--ringFocus ring
--radius-smCorner radius
--scale-3Padding
--scale-20Minimum height

Do / Don’t

Do

  • Use Textarea for genuinely multi-line content; use Input for anything single-line.
  • Let users resize vertically when content runs long.
  • Pair every Textarea with a Label, exactly as Input does.

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-describedby for every visible supporting message, aria-invalid="true" only while actually invalid, native disabled/readonly attributes.

Related Components / Patterns

  • Input — single-line text entry.
  • Label — the visible name of the field.