Textarea
A multi-line hand-drawn text field with an optional label, error message, and auto-resize.
Overview
A multi-line text field for collecting longer, free-form data, with an optional visible label,
(on React) an error message, and an opt-in autoResize mode that grows the field to fit its
content instead of scrolling.
Anatomy
An optional label above a sketchy-outlined text box; on React, an error message renders below the
box as a role="alert" element when error is set. The native resize handle is always disabled —
it visually collides with the hand-drawn border, so growth (when autoResize is set) is the only
way the box changes size.
Variants & States
| State | Status | Notes |
|---|---|---|
| Default | Implemented | — |
| Focused | Implemented | Stroke color swaps to a focus token. |
| Disabled | Implemented | — |
| Error / invalid | Implemented | React only — aria-invalid + aria-describedby + role="alert". Not implemented on Web Components or React Native, matching Input. |
| Auto-resize | Implemented | Opt-in via autoResize. Measures scrollHeight (or onContentSizeChange on React Native) in JS rather than CSS field-sizing, for cross-browser determinism. |
| Loading | Not implemented | — |
Web Components instead has required (native constraint validation via ElementInternals) —
React and React Native don’t expose that as a prop at all. These are two separate, real gaps, not
documentation omissions — same platform split as Input.
Usage
Do
- Use Textarea for free-form, multi-line input (comments, bios, feedback) — use Input for a single line of text.
- Set autoResize for short-to-medium free-form fields where scrolling inside a fixed box would hide content the user just typed.
Don't
- Don't rely on placeholder text as the only accessible name — it disappears once the user types.
- Don't expect autoResize to cap growth — v1 has no minRows/maxRows or a max-height-with-scroll option; the box grows to fit all typed content.
Accessibility
- React:
aria-invalid+aria-describedby(pointing at the error<span role="alert">) + RadixLabel/htmlForassociation. - Web Components:
label/forassociation;aria-labelfalls back to the placeholder when no label is set. No invalid-state ARIA exists today. - React Native:
labelis required (there is no separate label element on RN) and doubles asaccessibilityLabel;accessibilityState={{disabled}}has no error/invalid concept. - See the Accessibility Guide for general conventions.
Content
The label names the data being collected (“Bio”, “Feedback”), not an action — contrast with Button, whose label names an action. Error messages state what’s wrong and how to fix it.
Props API
React@ghds/react
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Visible label, associated via Radix Label. |
error | string | — | Error message. When set, marks the field invalid and announces it. |
autoResize | boolean | false | Grows the field to fit its content instead of scrolling. |
disabled | boolean | false | Disables editing. |
...rest | TextareaHTMLAttributes | — | All other native <textarea> attributes (value, onChange, rows, etc.) pass through. |
Web Components@ghds/web-components
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | '' | Current field value, mirrored to the form value. |
name | string | — | Submitted control name. |
placeholder | string | — | Placeholder text. |
label | string | — | Optional visible label, also the accessible name. |
rows | number | 2 | Visible row count — also the minimum height once measured. |
autoResize | boolean | false | Grows the field to fit its content instead of scrolling. Reflected as autoresize. |
disabled | boolean | false | Disables editing and form participation. |
required | boolean | false | Native constraint validation. No error prop exists. |
React Native@ghds/react-native
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Visible, accessible label — required (there is no separate label element on RN). |
value | string | — | Controlled text value. |
onChangeText | (text: string) => void | — | Change handler. |
placeholder | string | — | Placeholder text. |
rows | number | 2 | Visible row count — also the minimum height once measured. RN has no native rows attribute. |
autoResize | boolean | false | Grows the field to fit its content via onContentSizeChange. No error prop exists. |
disabled | boolean | false | Disables editing. |
testID | string | — | Test handle for queries. |
Code examples
// React — @ghds/react
import { Textarea } from '@ghds/react/textarea';
<Textarea
label="Bio"
placeholder="Tell us about yourself"
value={bio}
onChange={(event) => setBio(event.target.value)}
/>;
// React-only error state:
<Textarea label="Feedback" error="Feedback is required." />;
<!-- Web Components — @ghds/web-components -->
<script type="module">
import '@ghds/web-components/textarea';
</script>
<gh-textarea label="Bio" placeholder="Tell us about yourself" autoresize></gh-textarea>
<!-- No error prop; Web Components uses native `required` instead -->
<gh-textarea label="Feedback" required></gh-textarea>
// React Native — @ghds/react-native
import { Textarea } from '@ghds/react-native/textarea';
<Textarea
label="Bio"
placeholder="Tell us about yourself"
value={bio}
onChangeText={setBio}
autoResize
/>;
Live Demo
React’s demo includes the error state; the Web Components demo intentionally does not (it has no
error prop) and shows required instead. Both demos include an autoResize example.