Bridge view
Textarea
A multi-line plain-text editing control for free-form prose — comments, feedback, descriptions, support-ticket bodies. Native HTML `<textarea>` is canonical; carries `aria-multiline=true` implicitly. Distinct from TextInput by the multi-line value shape (line-breaks preserved on submission), by the `rows` axis, and by the auto-resize contract that grows the field as content fills.
Also called Multi-line text input Comment box Text area
When to use
Use
For free-form prose where the user may write more than a single line — comments, reviews, support-ticket bodies, feedback fields, descriptions. Use auto-resize when the natural input length is unpredictable and a fixed rows value either wastes space or forces internal scrolling too eagerly. Use fixed rows when the surrounding layout has firm vertical bounds (sidebar fields, dense forms). Pair with a character-counter when the field has a canonical maxlength that aids the user.
Avoid
For single-line inputs — that is `TextInput`. For structured input (rich-text, code, formula) — that is a dedicated rich-text editor or code-block editor; native textarea does not support inline formatting. For very short bounded prose (under ~40 chars) — TextInput plus soft-warning is leaner. Never use textarea where the value is conceptually a single line that may overflow — overflow handling on a single-line `<input>` is the correct surface.
Versus related
- text-input
`TextInput` is a single-line `<input type="text">` whose value never contains line breaks; `Textarea` is a multi-line `<textarea>` that preserves line breaks on submission. The axes diverge: textarea ships `rows`, `resize`, and auto-resize axes; text-input does not. Both share the label association rules and the form-integration discipline (Constraint Validation API, autocomplete tokens). Decision test: does the value carry meaningful line-breaks (Textarea) or is it a single-line value (TextInput)?
- combobox
`Combobox` commits one value from a constrained list with optional typeahead filter; `Textarea` accepts free-form arbitrary multi-line text. Combobox uses a listbox popup; Textarea is a flat field. Use Combobox for structured single-value choice; Textarea for unstructured prose entry.
Textarea is the canonical multi-line-prose-input primitive — a resizable rectangular field that accepts arbitrary line-broken text. Standalone canonical (not a TextInput variant) because the axes diverge: `rows`, `resize` direction, and the auto- resize contract have no analog in the single-line surface. Two variants — standard fixed-rows-with-manual-resize and auto-resize bounded by `minRows` plus `maxRows` — and an optional character-counter slot that announces remaining characters via `aria-live`. The reference documents the resize-blocks-zoom anti-pattern (a WCAG 1.4.10 reflow risk), the auto-resize-thrash performance pitfall, and the divergence from TextInput.
Figma↔Code mismatches
Where designer and developer worlds typically misalign on this component.
- 01 Figma
Textarea drawn with fixed pixel height
CodeTextarea sized by `rows` attribute (line-count) plus auto-resize for content-driven growth
ConsequenceDesigners ship "120 px tall textarea"; developers know the canonical surface is `rows` (line-count) which varies by font-size, line-height, and platform. The two surfaces diverge — the Figma frame height does not match the rendered textarea height across browsers (Mac vs Windows font-rendering shifts visible row count). Auto-resize variants expand beyond the Figma frame, breaking the design illustration.
CorrectDocument size as `rows` plus `size` token. Figma carries one frame per row count (3-row, 5-row, 8-row illustrative variants); auto-resize variant carries an annotation for `minRows` and `maxRows` bounds. Code uses the `rows` attribute or computes height from content.
- 02 Figma
Resize-handle drawn as a static decorative element
CodeResize-handle is a native browser affordance controlled by `resize` CSS property
ConsequenceDesigners paint a custom three-dot resize handle in the bottom-right corner; developers rely on the browser- provided handle (default) which renders differently across browsers and platforms. The two surfaces look different. Designers may not realise the handle is browser-controlled and try to position custom controls.
CorrectDocument the resize handle as browser-provided in the anatomy. Figma uses a generic visual placeholder (or omits it) with an annotation noting browser-rendering. Custom resize handles require completely overriding `resize: none` plus pointer-event handling — heavy and rarely worth the divergence from native.
- 03 Figma
Auto-resize variant drawn at a single representative height
CodeAuto-resize textarea grows from `minRows` to `maxRows` based on content
ConsequenceDesigners ship one auto-resize variant frame at "this is roughly the typical content height"; developers ship a textarea that ranges from 2 rows to 12 rows (or whatever bounds). The Figma file does not illustrate the empty / filled / overflow states, so designers cannot review the boundary behaviours.
CorrectDocument `minRows` and `maxRows` as canonical properties. Figma carries three frames (empty at minRows, filled at typical height, overflowing at maxRows-with-internal-scroll). Code computes height from content via `scrollHeight` measurement on input events.
- 04 Figma
Character-counter drawn as plain static text
CodeCharacter-counter is `aria-live="polite"` so SR announces the count on typing-pause
ConsequenceDesigners draw "120 / 500" as static text under the textarea; developers wrap it in a live-region. The visual is identical but the AT contract is invisible in the Figma file — designers may move the counter to a non-live span without realising the regression.
CorrectDocument the character-counter slot as `aria-live="polite"` in the anatomy. Figma's counter slot carries an annotation for the live-region behaviour. Code wraps the counter element in a live-region or sets `aria-live` on the element itself.
Variants, properties, states
Variants
Structurally different versions of the component.
standard auto-resize Properties
The same component, parameterised.
| Property | Type |
|---|---|
size | sm | md | lg |
resize | none | vertical | horizontal | both |
disabled | boolean |
readonly | boolean |
required | boolean |
invalid | boolean |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visibleactivedisabled |
data | emptyfilledinvalidat-limit |
Figma ↔ Code property map
| Figma | Kind | Code | Notes |
|---|---|---|---|
Variant | Enum | variant | standard (fixed `rows`) / auto-resize (grows from `minRows` to `maxRows` based on content). |
Size | Enum | size | sm / md / lg. |
Resize | Enum | resize | none / vertical / horizontal / both. Maps to CSS `resize` property; controls the browser-provided handle availability. Default `vertical` to keep WCAG 1.4.10 reflow intact. |
Rows | Number | rows | Initial line-count. For auto-resize variant, this is the `minRows` lower bound. |
Max Rows | Number | maxRows | Auto-resize-only — ceiling at which the textarea stops growing and switches to internal scrolling. |
Disabled | Boolean | disabled | — |
Readonly | Boolean | readonly | Prefer over `disabled` when the value is locked but should remain readable / tabbable / submittable. |
Required | Boolean | required | Pairs visual asterisk with the `required` attribute. |
Invalid | Boolean | invalid | Pairs `aria-invalid="true"` with the visual error treatment. |
Label | Text | children of `<label>` | Visible label text. Placeholder is not a substitute. |
Placeholder | Text | placeholder | Optional example value ("e.g. Steps to reproduce") — never the field meaning. |
Description | Text | description | Optional supporting prose, programmatically linked via `aria-describedby`. |
Error Message | Text | errorMessage | Visibility bound to invalid state. |
Character Count | Slot | characterCount | `aria-live="polite"` so SR announces on typing-pause. Threshold-visibility pattern (visible above ~75% of `maxlength`) is the GOV.UK canonical reference. |
State transitions
| From | To | Trigger |
|---|---|---|
empty | filled | User types into the textarea |
filled | empty | User clears the textarea content |
filled | invalid | Validation fails (over maxlength, under minlength, required-but-empty on submit) |
filled | at-limit | User reaches the character-count threshold (typically 75-100% of maxlength) |
Figma anatomy
| Slot | Figma type | Hint |
|---|---|---|
root | frame | Auto-layout vertical frame; label + textarea + description + character-count |
label | text | Text element above textarea; size matches form-label hierarchy |
textarea | frame | Multi-line text frame; min-height bound to rows token; resize-handle visual on bottom-end corner |
description | text | Smaller text below textarea; muted color; visibility per "has description" property |
error-message | text | Error-toned text below textarea; visibility per "has error" property |
character-count | text | Right-aligned smaller text below textarea; muted by default, error-toned when over limit |
Code anatomy
| Slot | Code slot | Semantic |
|---|---|---|
root | root | div-or-fieldset |
label | label | label-text |
textarea | textarea | textarea |
description | description | span-or-div |
error-message | error-message | span-with-aria-live |
character-count | character-count | span-with-aria-live |
Cross-framework expression
| Framework | Structure mechanism | Variant mechanism |
|---|---|---|
| Web Components | A `<ui-textarea>` host with `name`, `value`, `rows`, `auto-resize`, `min-rows`, `max-rows`, `disabled`, `required`, `invalid` attributes; renders native `<textarea>` slotted inside, plus optional label / description / character-count slots | attributes (`size="md"`, `resize="vertical"`, `auto-resize`); CSS `resize` property on the textarea drives the resize-handle availability; `:state(at-limit)` for state-driven styling |
| React | Function component (Material UI TextField with multiline, React Aria TextField with TextArea, Atlassian Textarea, Polaris TextField with multiline, Mantine Textarea); auto-resize via react-textarea-autosize or built-in props (`autoResize`, `minRows`, `maxRows`) | props with class-variance-authority; `multiline` boolean (Polaris idiom) or dedicated component (Atlassian); `value` plus `onChange` for controlled state |
| Angular (signals) | Angular component with input<string>('value'), input<boolean>('autoResize'), input<number>('minRows'), input<number>('maxRows'); ControlValueAccessor for ngModel/formControl integration; CDK TextFieldModule provides cdkTextareaAutosize directive | input<'sm' | 'md' | 'lg'>('size'); host-binding [attr.aria-invalid] from form-control state; signal- derived height for auto-resize via ResizeObserver |
| Vue | Single-file component with v-model for value (string); PrimeVue Textarea, Naive UI Input with type="textarea" as third-party precedents; auto-resize via composable or directive | defineProps with literal-union types; v-bind for rows / autoResize; computed property for aria-invalid from validation state |
Events
valueChangeheightChangeoptional
Form integration
- HTML5 validation
- Native Constraint Validation API surfaces `valueMissing` when `required` and empty, `tooLong` when over `maxlength`, `tooShort` when under `minlength`. The `pattern` attribute does NOT apply to textarea (HTML spec — pattern is input-only). For multi-line patterns, use `validate` callback in React Aria or custom-validation prop in form libraries. Custom validation surfaces via `setCustomValidity()` on the textarea element.
Performance thresholds
autoResizeRecalcIntervalinput-event-coalesce≥50msAuto-resize textareas measure content height on input events to update `height`. Without coalescing, hundreds of recalcs per second jank typing on slower devices (cursor lag, dropped frames). Throttle to one measurement per ~50 ms (or one per `requestAnimationFrame`) so the visual updates feel smooth without saturating the layout pipeline. Sources: react-textarea-autosize, autosize.js, Material UI TextField multiline implementation all coalesce.
maxLengthGuardrailcharacters≥100000charactersNative `<textarea>` accepts arbitrary content length, but DOM `value` getter / setter performance degrades with very large strings (~100k characters), and form submission of multi-megabyte text bodies stresses server validation and database storage. For structured-content surfaces (rich-text, code, formula), use a dedicated editor; for prose surfaces, set a sensible `maxlength` and surface the limit via the character-counter slot.
Internationalisation
RTL · mirroring
Textarea inherits `dir` from the document or accepts explicit `dir` attribute. Under `direction: rtl`, the caret starts at the inline-end (right side) and text flows right-to-left. Resize handle position swaps to the bottom-inline-start corner via logical positioning. The character-counter alignment (typically right-end in LTR) flips to left-end via `text-align: end`.
Text expansion
Label text expands 30-50% under translation. Description and error-message expand similarly; reserve no fixed width on text slots. The textarea field itself accommodates expansion naturally because the value is user-supplied, not translated. Character-counter text ("X / Y characters" → "X / Y caractères" or "X / Y Zeichen") follows generic prose expansion. Auto-resize variants need no special handling for expansion because the height adapts to content.
Accessibility
| Slot | Accessibility hint | |
|---|---|---|
root | The root holds programmatic associations between the textarea and its label / description / error / character-count via `for` and `aria-describedby`. No ARIA role is needed on the root itself — the textarea carries the textbox role natively. | |
label | Label text is the textarea's accessible name. SR users hear "Comments, edit text, blank, multi-line" or the equivalent per the SR. Required-field markers (visual asterisk) live in the label slot but the canonical signal is `required` attribute on the textarea, not the visual asterisk. | |
textarea | Native `<textarea>` provides the role and the `aria-multiline=true`. Never substitute `<div contenteditable="true">` for a textarea — the contenteditable surface lacks form-submission participation, lacks placeholder-and-required attributes, and ships inconsistent SR support across browsers. `aria-invalid="true"` plus `aria-describedby` linking to error-message activate validation surfacing. | |
description | Associate via `aria-describedby` on the textarea. SR announces description after the label and the field state. Avoid using description for required-field markers — use `required` attribute on the textarea plus visible asterisk in the label. | |
error-message | `aria-describedby` on the textarea includes the error- message id; `aria-invalid="true"` triggers SR error announcement on focus. The error-message element carries `aria-live="polite"` so async-validation errors announce when they appear without forcing focus moves. | |
character-count | `aria-live="polite"` so the counter announces on typing-pause without interrupting active typing. `aria-describedby` on the textarea may include the counter id so SR users hear the limit on field focus. Avoid `aria-live="assertive"` — interrupting every keystroke is hostile. |
Accessibility acceptance
Keyboard walk
| Keys | Expected |
|---|---|
Tab | Focus moves to the textarea. Focus ring renders on the field. The textarea is its own tab stop. |
Enter (focus inside textarea) | Inserts a newline in the value. Does NOT submit the form (unlike single-line `<input>` where Enter submits). For ctrl+Enter or cmd+Enter to submit, the consumer wires the keydown handler externally — this is convention in chat / comment surfaces but not canonical to the textarea primitive. |
Shift+Tab | Moves focus to the previous focusable element. The textarea does not capture Tab — Tab inserts a tab character only if the consumer explicitly opts in (rare; generally Tab moves focus per HTML default). |
Resize handle drag (mouse) | When `resize: vertical | horizontal | both`, the bottom-end corner shows a resize affordance. Drag adjusts the textarea height (and width if applicable). Keyboard users have no equivalent canonical mechanism — auto-resize variants make this point moot by growing automatically with content. |
Screen-reader announcements
| Trigger | Expected |
|---|---|
| SR encounters an empty textarea | "[label text], edit text, multi-line, blank" or the SR-equivalent. The role (textbox), the multi-line property, and the empty state all announce. |
| SR encounters a filled textarea | "[label text], edit text, multi-line, [first line of content]" — most SR read the first line plus position; users navigate with Down arrow to read further lines. |
| User types and reaches the character-count threshold | Character-counter announces via `aria-live="polite"` on typing-pause — "20 characters remaining" or "5 characters over the limit". The textarea's `aria-invalid` flips when the limit is crossed. |
| SR encounters a textarea with validation error after submit | "[label text], edit text, multi-line, invalid entry, [error message]". The error-message announces via `aria-describedby` plus `aria-invalid`. |
axe-core rules to assert
aria-allowed-attraria-required-attrlabelcolor-contrastautocomplete-valid
Same data as JSON for direct ingestion into Playwright + @axe-core/playwright or Jest + jest-axe:
/api/components/textarea/a11y-fixture.json
Contracts
Non-negotiable contracts
APGWAI-ARIA textbox + label patterns The textarea has a programmatically-associated label — either `<label for="<id>">` or a wrapping `<label>`. Adjacent prose and placeholder text are not substitutes. The label is the canonical accessible name.
Without the association, SR users hear "edit text, multi-line" without the field meaning. The programmatic contract is what AT relies on; visual proximity is sighted-only and breaks under voice navigation, mobile SR, and accessibility tooling.
WCAGWCAG 1.4.10 Reflow `resize: none` is restricted to layouts that auto- resize themselves OR fields with short `maxlength` bounds. Default `resize: vertical` so users with zoom requirements (WCAG 1.4.10 Reflow) can adjust the field. Implementations that lock resize without compensating for low-vision zoom block accessible content entry.
Without resize freedom, low-vision users at 200% zoom encounter content that does not fit or is truncated. The reflow contract is one of the canonical 2.x AA requirements; locking resize is the most common single textarea regression in accessibility audits.
Canon Auto-resize implementations coalesce height-recalc to one measurement per ~50 ms (or per `requestAnimationFrame`). Recalculating on every input event without throttle drops frames on commodity hardware and breaks typing cadence on slower devices.
Without coalescing, textareas with auto-resize become unusable on tablets, low-end Android, and older laptops. The performance threshold is invisible in static design review but surfaces immediately in real-device QA — and by then the implementation is harder to fix.
Vocabulary drift
- HTML
`<textarea>`- Native HTML element. `aria-multiline=true` is implicit; do not set explicitly. Default `rows="2"` per spec; canonical practice sets a meaningful initial size. Line-breaks preserve on form submission as `\n`.
- WAI-ARIA
`role="textbox"` (with `aria-multiline=true`)- WAI-ARIA textbox role. Native textarea provides this implicitly; custom-rendered multi-line surfaces (rare — `contenteditable` is the alternative) need the role plus `aria-multiline` plus key handling.
- Material 3
Text field (multi-line variant)- Material 3 codifies multi-line as a variant of the text field component, not a separate primitive. Same canonical contract; the spec splits multi-line guidance into a dedicated specs page.
- Polaris
TextField (multiline prop)- Polaris exposes multi-line as a `multiline` boolean prop on TextField. Same canonical contract; the prop activates auto-resize and the dedicated textarea anatomy.
- Atlassian
Textarea- Atlassian ships Textarea as a separate component with `resize`, `minimumRows`, `maxHeight` props. Canonical name match; the auto-resize bounds (minimumRows + maxHeight) align with the canonical `minRows` + `maxRows` axis.
- GOV.UK
Textarea + Character count- GOV.UK ships Textarea plus a separate Character count component (textarea with built-in counter). The canonical anatomy folds character-count as an optional slot; GOV.UK's threshold-visibility pattern is a canonical optimisation captured in the slot prose.
Common mistakes
#textarea-no-label
Textarea has no programmatically-associated label
The textarea renders with no `<label for>`, no wrapping `<label>`, and no `aria-labelledby`. SR users hear "edit text, multi-line, blank" without the field meaning. Form filling becomes blind for AT users.
Wrap the textarea in `<label>` (the textarea becomes a child of the label) or pair it with `<label for="<id>">`. Either form establishes the canonical association. Adjacent paragraphs and placeholder text are NOT substitutes — the AT contract is programmatic.
#textarea-placeholder-as-label
Field meaning lives in placeholder text instead of a label
The textarea has no visible label; placeholder text ("Tell us more...") communicates the meaning. When the user starts typing, the placeholder disappears and the meaning is lost. SR may or may not announce the placeholder; users with cognitive load or short-term memory needs cannot recall what the field asks.
Always render a visible `<label>` with the field meaning. Placeholder text is reserved for example content ("e.g. Steps to reproduce") that supplements but does not replace the label. The visible label stays present; the placeholder is auxiliary.
#textarea-resize-blocks-zoom
`resize: none` plus narrow content area blocks WCAG 1.4.10 reflow
The textarea is rendered with `resize: none` to fit a tight design layout. On narrow viewports or under user zoom (e.g. 200% zoom for low-vision users), the textarea content overflows or becomes unreadable because the user cannot enlarge the field. Violates WCAG 1.4.10 (Reflow).
Default to `resize: vertical` so users can extend the field. Reserve `resize: none` for layouts where the surrounding context auto-resizes (sidebar panels, full- bleed modals) AND the field's `maxlength` is short enough that overflow is unlikely. Test at 200% zoom to verify content remains readable.
#textarea-autoresize-thrash
Auto-resize recalculates layout on every keystroke without debounce
Auto-resize textarea measures `scrollHeight` and updates its CSS `height` on every `input` event. On long-form content with rapid typing, the layout recalculates hundreds of times per second — janks the input, drops frames, and on slower devices breaks typing cadence. Visible as cursor lag and stuttering.
Debounce the height-recalc via `requestAnimationFrame` coalescing (one measure per frame) or via input-event throttling (one measure per ~50 ms). Alternative mechanism: hidden mirror element with the same content and CSS, measured via offsetHeight without triggering layout-recalc on the textarea itself. Both patterns are well-documented in mature libraries (react-textarea- autosize, autosize.js).
#textarea-character-count-no-aria-live
Character-counter updates silently for SR users
The character-counter ("120 / 500") renders as static text and updates as the user types, but is not in a live-region. SR users hear the field but never the counter; they cannot tell when they are approaching the limit and may submit invalid content.
Wrap the counter in `aria-live="polite"` so SR announces the count on typing-pause. For threshold- visibility patterns (GOV.UK), make the count visible and live only above ~75% of the limit so AT users get the warning when it matters. Avoid `aria-live="assertive"` — every keystroke announcement is hostile.