Textarea
betaA multi-line text field that grows with its text, with sizes and an error state.
Preview
size
states
Tell us what went wrong so we can fix it.
field
fixed
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/textarea
Use it for
- Text that can run past one line: a comment, a bio, a bug report.
- Name it with a visible label. Give the textarea an id and point the label's htmlFor at it.
- Let it grow with the text. For a fixed height, add className='field-sizing-fixed' and set rows.
- Show errors with aria-invalid plus a message under it, linked with aria-describedby.
Avoid it for
- One-line values like a name or an email address. Use an input.
- Text that needs bold, lists, or links. Use a rich text editor.
- Turning Enter into submit. People expect Enter to start a new line here.
- Using placeholder as the label. It disappears as soon as someone types.
Props
- size
Minimum height, padding, and text size. Density scales them.
"sm" | "md" | "lg" · default "md"
- rows
Visible lines. It only applies with className='field-sizing-fixed', because the default grows with the text.
number
- disabled
Blocks typing and removes the textarea from the tab order.
boolean · default false
- aria-invalid
Shows the danger border. Set it when validation fails, and link the message with aria-describedby.
boolean
- onValueChange
Called with the new text on every change. Use it with value for a controlled textarea.
(value: string, details) => void
- className
Extra classes. Later classes win, so className='field-sizing-fixed' stops it from growing.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| size | "sm" | "md" | "lg" | "md" | Minimum height, padding, and text size. Density scales them. |
| rows | number | - | Visible lines. It only applies with className='field-sizing-fixed', because the default grows with the text. |
| disabled | boolean | false | Blocks typing and removes the textarea from the tab order. |
| aria-invalid | boolean | - | Shows the danger border. Set it when validation fails, and link the message with aria-describedby. |
| onValueChange | (value: string, details) => void | - | Called with the new text on every change. Use it with value for a controlled textarea. |
| className | string | - | Extra classes. Later classes win, so className='field-sizing-fixed' stops it from growing. |
Accessibility
- Renders a native <textarea>, so Enter adds a new line and typing, selection, and autofill work as usual.
- It has no name on its own. Pair it with a <label htmlFor>, or wrap it in a label.
- aria-invalid="true" tells screen readers the value is wrong. Put the reason in text next to it, not only in the red border.
- The border uses --input, which the theme tests hold to 3:1 against the page and cards.
- The focus ring uses --ring, which the theme tests hold to 3:1 against the page and cards.
- On touch screens the text is 16px, so iOS Safari does not zoom in when the textarea gets focus.
Examples
With a label
<div className="grid gap-2">
<label htmlFor="bio" className="text-sm font-medium">
Bio
</label>
<Textarea id="bio" name="bio" placeholder="A few words about you" />
</div>Error message the textarea points to
<div className="grid gap-2">
<label htmlFor="reason" className="text-sm font-medium">
Reason for the refund
</label>
<Textarea id="reason" aria-invalid aria-describedby="reason-error" />
<p id="reason-error" className="text-sm text-destructive-soft-foreground">
Tell us what went wrong so we can fix it.
</p>
</div>Fixed height
<Textarea aria-label="Release notes" className="field-sizing-fixed" rows={8} />