Dev 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.

Highlight
Fig 1.1 · Textarea · Dev view
Dev

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
Both

Variants, properties, states

Variants

Structurally different versions of the component.

standard auto-resize

Properties

The same component, parameterised.

PropertyType
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).

KindStates
interactive
hoverfocus-visibleactivedisabled
data
emptyfilledinvalidat-limit
Both

State transitions

FromToTrigger
emptyfilledUser types into the textarea
filledemptyUser clears the textarea content
filledinvalidValidation fails (over maxlength, under minlength, required-but-empty on submit)
filledat-limitUser reaches the character-count threshold (typically 75-100% of maxlength)
Dev

Cross-framework expression

FrameworkStructure mechanismVariant 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
Both

Events

  1. valueChange
    Payload
    `{ value: string, previousValue: string }`. Fires when the textarea content changes via user input (typing, paste, cut). Multi-line values preserve `\n` line- breaks. Programmatic value changes (controlled-prop updates) typically suppress the event to avoid loops.
    Web Components
    `valueChange` CustomEvent on the host with `event.detail = { value, previousValue }`.
    React
    `onChange(event)` (native) or `onChange(value)` (controlled idiom — React Aria, Material UI).
    Angular Signals
    `output<string>('valueChange')` plus ControlValueAccessor `onChange(value)` for forms.
    Vue
    `@update:modelValue` event for v-model binding; `@input` for non-controlled.
  2. heightChangeoptional
    Payload
    `{ height: number, rows: number }`. Fires when the auto-resize textarea adjusts its height in response to content changes. Only canonical when consumers track layout reflow externally; standalone auto-resize manages height internally without exposing the event.
    Web Components
    `heightChange` CustomEvent with `event.detail = { height, rows }`.
    React
    `onHeightChange(height, rows)` (react-textarea- autosize idiom).
    Angular Signals
    `output<{ height: number; rows: number }>('heightChange')`.
    Vue
    `@height-change` event with payload `{ height, rows }`.
Dev

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.
Dev

Performance thresholds

  • autoResizeRecalcIntervalinput-event-coalesce50ms

    Auto-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.

  • maxLengthGuardrailcharacters100000characters

    Native `<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.

Both

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.
Both

Accessibility acceptance

Keyboard walk

KeysExpected
TabFocus 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+TabMoves 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

TriggerExpected
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 thresholdCharacter-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-attr
  • aria-required-attr
  • label
  • color-contrast
  • autocomplete-valid

Same data as JSON for direct ingestion into Playwright + @axe-core/playwright or Jest + jest-axe: /api/components/textarea/a11y-fixture.json

Both

Contracts

Non-negotiable contracts

  1. 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.

  2. 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.

  3. 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.
Dev

Common mistakes

Blocker

#textarea-no-label

Textarea has no programmatically-associated label

Problem

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.

Fix

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.

Major

#textarea-placeholder-as-label

Field meaning lives in placeholder text instead of a label

Problem

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.

Fix

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.

Major

#textarea-resize-blocks-zoom

`resize: none` plus narrow content area blocks WCAG 1.4.10 reflow

Problem

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).

Fix

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.

Major

#textarea-autoresize-thrash

Auto-resize recalculates layout on every keystroke without debounce

Problem

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.

Fix

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).

Major

#textarea-character-count-no-aria-live

Character-counter updates silently for SR users

Problem

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.

Fix

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.

Figma↔Code mismatches
  1. 01
    Figma

    Textarea drawn with fixed pixel height

    Code

    Textarea sized by `rows` attribute (line-count) plus auto-resize for content-driven growth

    Consequence

    Designers 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.

    Correct

    Document 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.

  2. 02
    Figma

    Resize-handle drawn as a static decorative element

    Code

    Resize-handle is a native browser affordance controlled by `resize` CSS property

    Consequence

    Designers 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.

    Correct

    Document 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.

  3. 03
    Figma

    Auto-resize variant drawn at a single representative height

    Code

    Auto-resize textarea grows from `minRows` to `maxRows` based on content

    Consequence

    Designers 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.

    Correct

    Document `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.

  4. 04
    Figma

    Character-counter drawn as plain static text

    Code

    Character-counter is `aria-live="polite"` so SR announces the count on typing-pause

    Consequence

    Designers 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.

    Correct

    Document 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.