Textarea
A multi-line text field for longer freeform input.
@garn/ui/textareaDefault
Default textarea.
Appearance
The shared field-surface fill axis (see lib/field-variants.ts; identical to Input): outline = neutral border on transparent fill; soft = subtle fill that clears to the outline on focus; ghost = seamless until focus.
Outline
outlineSoft
softGhost
ghostSizing
Sets padding + a min-height of ~2 control rows; tracks the same grid as Input.
xssmmdlgxlSpacing
The same control rendered across garn's density modes — compact, default, and spacious.
States
Interactive states, toggled live.
<Textarea />State
Labeled
Textarea with a label.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add textarea- Registry
textarea- Source
- textarea.tsx
- Deps
class-variance-authority- Registry deps
utilsfield-variantsuse-resize-observeruse-isomorphic-layout-effect
Import
import { Textarea } from "@garn/ui/textarea";Props
Generated from the component's TypeScript types.
appearance- Description
- No description.
- Type
"ghost" | "outline" | "soft"- Default
outline
size- Description
- No description.
- Type
"lg" | "md" | "sm" | "xl" | "xs"- Default
md
autoResize- Description
- Grow to fit content instead of scrolling — re-measured on input and on width changes (wrapping shifts height). The `size` min-height stays the floor; set a `max-height` class to cap it (it starts scrolling past the cap).
- Type
boolean
Plus 294 inherited native <textarea> attributes.
Styling
Target these data-slots and remap these tokens to restyle without forking the component.
[data-slot="textarea"]| Property | Token | Tier |
|---|---|---|
minHeight | --garn-control-h-md | semantic · air |
paddingInline | --garn-control-px-md | semantic · air |
borderColor | --garn-foreground | semantic |
Overview
Textarea collects multi-line freeform text — a comment, a description, a message. It renders the same field surface as Input (appearance × size), so it sits on the shared grid and reddens on aria-invalid like every other control; its minimum height is derived as two control rows so it reads as multi-line at rest. For a single line, use Input; for a fixed-length code, InputOTP; for a choice, Select.
Guidelines
Always pair a Textarea with a Label, and size it to the expected input. Start tall enough to signal that a few sentences are welcome; a single-row box invites single-word answers. Use the placeholder for an example, never as the field's name.
Show limits before they're hit, not after. When there's a max length, surface a live character count and warn as it approaches the cap — don't silently truncate on submit. Wire validation through aria-invalid + a described error message so the state is announced.
Let users grow the field. Allow vertical resize (or auto-grow) for longer entries rather than trapping a long message in a small scroll box. Don't disable the browser's resize handle without giving an equivalent way to see more.
Best practices
- Pair with a label; allow vertical resize unless the layout requires a fixed height.
- Set a sensible number of rows / min-height for the expected content.
- Don't use a textarea for one-line values.
- Don't disable resizing without a layout reason.
Content guidelines
- Use the placeholder for a brief example of the expected input, never as the label.
- If there's a length limit, state it near the field and show the remaining count.
- Keep helper text to one line of guidance — not instructions on how to type.
Accessibility
- Role
textbox- Focus
- Border darkens (foreground/40) on any focus; keyboard focus adds a 2px neutral ring (foreground/15).
Enforceable contracts
Give every textarea an accessible label (a <label htmlFor>, or aria-label/aria-labelledby).
when no associated <label htmlFor>, aria-label, or aria-labelledby
- • Associate a visible <label> (htmlFor) or provide aria-label / aria-labelledby.
- Status
- Stable
- Version
v0.3.0- Introduced
v0.1.0
garn doesn't yet record a per-release version history. This reflects the component's current lifecycle state; full changelog entries will appear once the metadata pipeline emits a lifecycle.history field.