Overview
<Textarea /> is the canonical multi-line text-input primitive. It pairs with <label> for the
accessible name and shares the same form-control surface as <Input /> — variants, sizes, colors,
focus ring, and invalid state — extended for multi-line use with auto-resize, character counts,
and manual resize affordances.
Overview — placeholder, content, and validation
Variants
Same four "border + background" stories as Input — by design, the two components are visually indistinguishable in the resting state of a form. Adding a variant to Input adds it to Textarea (the type is a literal alias, and the recipe imports the same compound rows).
outline— 1px border, paper background. Default; safest on any surface.solid— filled tint, no visible border. Best when the parent is alreadybg.paper.ghost— invisible at rest; gains a tinted border + faint bg on hover / focus.underline— Material-style single bottom rule, dense forms.
Every variant works with every color (7 colors). The focus ring + focused border use the
active color; the body border stays neutral at rest so colored textareas don't shout for
attention.
Anatomy
[ <textarea/> ] ← inner element, padding + leading-relaxed + resize
[ counter (optional) ] ← absolute end-bottom, pointer-events-none[ <textarea/> ] ← inner element, padding + leading-relaxed + resize
[ counter (optional) ] ← absolute end-bottom, pointer-events-noneMulti-line-specific concerns
autoResize(trueby default) grows the textarea with content, clamped betweenminRowsandmaxRows. Once content exceedsmaxRows, an internal scrollbar takes over.resize('vertical'by default) controls the native CSSresizeproperty — the browser-rendered corner grip. Works alongsideautoResize: a user can still drag below the auto-resize ceiling unlessresizeis'none'.showCount(paired withmaxLength) renders a small bottom-end counter. When the limit is hit,data-at-limit="true"flips the counter text to the danger token — consumers can CSS-target this for custom animation.
Examples
Basic
Variants
Sizes
AutoResize
WithCount
ResizeModes
Invalid
Disabled
Controlled
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| autoResize | boolean | true | Grow the textarea with its content. Off-the-shelf modern UX — opt out only for fixed-height layouts where the surrounding chrome shouldn't shift. |
| color | ResponsiveValue<InputColor> | 'primary' | Accent for the focus ring + focused border. |
| fullWidth | ResponsiveValue<boolean> | true | Stretch to fill the parent's inline-size. |
| invalid | boolean | — | Visual + `aria-invalid` invalid state. Wins over the active `color` for border + ring. |
| maxRows | number | — | Ceiling for `autoResize`. Once content exceeds this many lines the textarea stops growing and an internal scrollbar takes over. Omit for no ceiling. |
| minRows | number | — | Floor for `autoResize`. The textarea won't shrink below this many lines regardless of how empty it gets. Defaults to `rows`. |
| resize | enum | 'vertical' | Manual resize affordance. The native CSS `resize` property: `'none'` hides the corner grip, `'vertical'` (default) is the modal expectation, `'horizontal'` is unusual, `'both'` is the browser default. |
| rows | number | 3 | Initial visible row count. Used as the textarea's natural height before `autoResize` kicks in. When `autoResize` is off this is the static height. |
| showCount | boolean | — | Render the bottom-end character counter. Pairs naturally with `maxLength` — when both are present the counter reads `current / max` and flips `data-at-limit` once the cap is hit. Without `maxLength`, just the current length is shown. |
| size | ResponsiveValue<TextareaSize> | 'md' | Vertical padding + font-size scale. |
| sx | Sx | — | Theme-aware inline style object (resolves palette / spacing / radius tokens to CSS vars). |
| variant | ResponsiveValue<InputVariant> | 'outline' | Border + background story. |
Accessibility
- Renders a native
<textarea>— keyboard interaction, IME composition, and form submission all use the platform's built-in behaviors. - Labels: Textarea never auto-labels. Wrap with
<label htmlFor>(or the future<Field>), passaria-label, or passaria-labelledby. A dev-mode warning fires when none of those are present —[apx-ds][TEXTAREA_NO_LABEL]. - Invalid state:
invalid={true}setsaria-invalid="true"on the textarea and a matchingdata-invalidon the wrapper. The danger border + ring come from the same attribute selector Input uses, so a future<Field invalid>propagates without prop-drilling. aria-describedbyis preserved as-is. The character counter isaria-hidden="true"(the count is decoration — the textarea itself announces value changes natively). Consumers who want screen-reader announcement of the count should render a<span aria-live="polite">separately and wire it viaaria-describedby.- Disabled vs read-only:
disabledis unreachable by tab;readOnlyis reachable but uneditable. - Focus ring lives on the wrapper (
focus-within:ring-*) so the counter footer is inside the same visual frame as the textarea.
Theming
Both root (wrapper), textarea (inner element), and count (counter footer) are addressable
slots. Tweak the global theme to remix the whole library at once, or scope overrides to Textarea
specifically:
<ThemeProvider
theme={defineTheme({
components: {
Textarea: {
defaultProps: { autoResize: false, rows: 6 },
styleOverrides: {
root: 'shadow-xs',
textarea: 'font-mono leading-snug',
count: 'text-fg',
},
},
},
})}
>
{children}
</ThemeProvider><ThemeProvider
theme={defineTheme({
components: {
Textarea: {
defaultProps: { autoResize: false, rows: 6 },
styleOverrides: {
root: 'shadow-xs',
textarea: 'font-mono leading-snug',
count: 'text-fg',
},
},
},
})}
>
{children}
</ThemeProvider>Instance-level overrides win via tailwind-merge:
<Textarea className="font-mono" sx={{ radius: 'xl' }} style={{ minHeight: 240 }} /><Textarea className="font-mono" sx={{ radius: 'xl' }} style={{ minHeight: 240 }} />Do / Don't
- Do pair every Textarea with a visible
<label>. Placeholders are not labels. - Do use
maxLength+showCountwhen there's a hard cap so users know in advance. - Don't use
disabledfor "not editable right now" —readOnlykeeps the field reachable. - Don't turn off
autoResizeunless you have a strong layout reason; modern forms expect the field to grow with content.