Textarea

User Experience (UX)

How a textarea behaves for every person, input and setting, so it's accessible, predictable, clear in every state and adaptive.

Now let's look at how a textarea behaves for every person, input and setting. A good textarea is accessible, predictable, designed in every state and adaptive.

Accessible

Use the native element

A native <textarea> gives you focus, text editing, new lines, selection, copy and paste, undo, spellcheck, autofill, scrolling, resizing and form submission, all for free. A contenteditable element with role="textbox" only gives you the editing and a role. It isn't submitted with its form, autofill skips it, pasting brings in the formatting of wherever the text came from, and screen readers and voice control handle it less reliably than a native field, so every one of those is one more thing to rebuild by hand.

{/* Do: you get editing, new lines, autofill and form submission for free */}
<label htmlFor="message">Message</label>
<textarea id="message" name="message" rows={4} />

{/* Don't: there's no form submission or autofill, and pasting brings in formatting */}
<div className="textarea" role="textbox" aria-multiline="true" contentEditable onInput={update} />

If people need formatting, like bold text and lists, that's a rich text editor, which is a different component with a lot more to it. For plain text, keep to the <textarea>.

Label every textarea

The label is also the textarea's accessible name. Screen readers read it when the textarea gets focus, voice control users say it out loud ("click Message"), and tests and agents use it to find the textarea.

Include what you were doing, and any error message you saw

<label htmlFor="details">What went wrong?</label>
<p id="details-hint">Include what you were doing, and any error message you saw</p>
<Textarea id="details" rows={4} aria-describedby="details-hint" />
  • Every textarea has a visible <label>, linked to it with for and the textarea's id (WCAG 1.3.1, 3.3.2). Clicking the label focuses the textarea too.
  • A hint, like what to include or how long it can be, goes in the textarea's description, which screen readers read after its name. You can do that with aria-describedby, pointing at the hint's id. The Textarea adds its count after it.
  • If you name a textarea with aria-label or aria-labelledby, make sure the name includes the visible label, ideally at the start (WCAG 2.5.3).
  • The Textarea doesn't make up IDs of its own (see Deterministic), so give it an id for its label to point at, and one to each hint and error message that points back at it.

A placeholder isn't a label. It disappears as soon as people start writing, so they can't check what they were asked for, it's lighter than the text around it, so it's harder to read, and it isn't a reliable name for assistive technologies. In a textarea, a long placeholder also looks like text that's already been written. If you use one at all, keep it to a short example, and put anything people need to know in the hint.

Contrast and size

  • The text needs a contrast of at least 4.5:1 with its background, and so do the placeholder and the count, since people need to read them too (WCAG 1.4.3). Disabled textareas are exempt, but they should still be readable.
  • The border needs 3:1 against the page (WCAG 1.4.11). An empty textarea is nothing but its border, so it's how people see that there's something to fill in. The focus ring needs 3:1 as well.
  • Every textarea is at least 24px tall (WCAG 2.5.8), and on touch screens at least 44px (WCAG 2.5.5), however few rows it has.

Show focus

When a textarea has focus, it shows an outline in the same focus colour as every other component (WCAG 2.4.7). Like the Button's and the Input's, it's at least 2px thick and drawn outside the border, and we recommend 2px for both. Browsers match :focus-visible on a textarea however it got focus, since people are about to write either way, so the ring shows after a click too.

You'll also want to keep focused textareas clear of sticky headers and footers, which you can do with scroll-padding (WCAG 2.4.11, 2.4.12). On phones, the on-screen keyboard covers the bottom of the page too, and an auto-growing textarea gets taller as people write, so check that the line they're writing on stays in view above it.

Predictable

Leave typing to the browser

The Textarea leaves text editing to the browser, so moving the caret, selecting, copying, cutting, pasting, undoing and spellchecking all work just as they do everywhere else. We recommend never taking any of them over, and never blocking paste. People write longer text somewhere else and paste it in, like a cover letter or a bug report, and blocking paste makes them type it all again by hand.

Keyboard

KeyTextarea
Tab, Shift+TabMove focus to and from the textarea
Arrows, Home and EndMove the caret, line by line and within a line
EnterStarts a new line, and never submits its form

Enter starts a new line in a textarea, which HTML leaves out of implicit submission, so a form with a textarea needs a submit button that people can reach with Tab. We recommend never making Enter send the form, even in a chat. People press it for a new line out of habit, and a message sent half-written can't be taken back. If you'd like a shortcut to send, use one that can't be pressed by accident, like Ctrl+Enter or Cmd+Enter, and say what it is next to the submit button.

Tab moves focus out of a textarea rather than typing a tab, as it should, so keyboard users never get stuck in it (WCAG 2.1.2). If your textarea is for code, where people expect Tab to indent, make sure Escape then Tab still moves focus on, and say so in the hint.

Don't cut people off

maxlength stops people typing past a limit, but it also silently cuts off what they paste, and they may never notice that the end is missing. The Textarea's limit counts instead. People can type and paste past it, the count says by how much, and the textarea is marked invalid until they've cut it back, so they decide what to keep. Check the limit on the server too, since the count only helps people stay within it.

The count is a status message, so screen reader users hear it without leaving the textarea (WCAG 4.1.3). It would talk over them if it were announced on every keystroke, though, so the Textarea waits until people pause for a second, and only announces it once they've used 80% of the limit, or gone over. It goes through the same polite live region as the Button's loading announcement, one for the whole page, which you can read about under the Button's Loading.

Help people fill it in

  • If a textarea asks for the person's own address, give it autocomplete="street-address", which the HTML standard keeps for a multi-line address (WCAG 1.3.5). Browsers then fill it in for them, which helps everyone, and most of all people who find typing hard.
  • Leave spellcheck on for prose, as browsers already do for a textarea. Turn it off with spellcheck="false" only for text that isn't words, like code or a list of IDs.
  • Keep what people have written if something goes wrong, like a failed submit or a page they leave by accident. Losing a long message is far worse than losing a single field.

Every state

Disabled

A disabled textarea can't be focused, edited or submitted. disabled sets the native attribute, so the textarea leaves the tab order, screen readers announce it as dimmed or unavailable when they read through the page, and its form leaves its text out when it's sent. It can't be resized either, since nobody can write in it, so there's nothing to make room for. If people need to read or copy the text, and the form needs to send it, the textarea should be read-only instead.

We recommend disabling as little as you can, since a disabled textarea can't explain why it's disabled, and keyboard users can't even reach it to find out. Never disable one while it has focus, either, like while its form is being sent. Disabling a focused textarea takes its focus away, and keyboard and screen reader users lose their place. Make it read-only while the form is sent instead.

Read-only

A read-only textarea shows text that people can't change, like the terms of an agreement or a message they've already sent. It stays in the tab order, so people can focus it, scroll it, select the text and copy it, screen readers announce it as read-only, and its form still sends it. readonly is a native attribute, so it lands on the <textarea> like any other. We recommend a dashed border, which tells it apart from a textarea you can write in, while the text keeps its full contrast. If the text never changes, a paragraph might be a better fit.

Invalid

An invalid textarea has text that needs fixing, and an error message that says how. The Textarea shows the state, and the page decides when to show it, since only the page knows what makes the text right. The one exception is its limit: over it, the Textarea marks itself invalid, and its count says by how much.

You can enter up to 200 characters

200 characters left

import { useRef, useState, type FormEvent } from "react";
import { flushSync } from "react-dom";

const LIMIT = 200;

export function Contact() {
  const [message, setMessage] = useState("");
  const [error, setError] = useState("");
  const [status, setStatus] = useState("");
  const textarea = useRef<HTMLTextAreaElement>(null);

  function update(value: string) {
    setMessage(value);
    // Once an error shows, check again as people type, so it clears as soon as it's fixed.
    if (error) setError(validateMessage(value, LIMIT));
  }

  async function send(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const text = validateMessage(message, LIMIT);
    if (text) {
      // Focus once the error is in the page, so screen readers read it with the textarea.
      flushSync(() => {
        setError(text);
        setStatus("Couldn't send your message. Check what you've written.");
      });
      textarea.current?.focus();
      return;
    }
    await api.send(message);
    setStatus("Message sent");
  }

  return (
    <form noValidate onSubmit={send}>
      <label htmlFor="message">Message</label>
      <p id="message-hint">You can enter up to {LIMIT} characters</p>
      <Textarea
        ref={textarea}
        id="message"
        limit={LIMIT}
        required
        value={message}
        aria-invalid={error ? true : undefined}
        aria-describedby={error ? "message-error message-hint" : "message-hint"}
        onChange={(event) => update(event.target.value)}
      />
      {error && <p id="message-error">{error}</p>}
      <Button type="submit" color="primary">Send</Button>
      <p role="status">{status}</p>
    </form>
  );
}

validateMessage stands in for your own check, which returns an error message, like "Enter a message", or "Message must be 200 characters or fewer" over the limit, and an empty string when the message is fine. Try pressing Enter in the example: it starts a new line, and only Send sends the form.

  • Set aria-invalid="true" on the textarea while its error message shows, and point aria-describedby at the message, before the hint. Screen readers then announce the textarea as invalid, and read the message right after its name (WCAG 3.3.1).
  • Say what's wrong and how to fix it, as in "Message must be 200 characters or fewer", rather than "Invalid input" (WCAG 3.3.3). A red border alone can't say any of that, and it means nothing to people who can't tell red apart (WCAG 1.4.1).
  • Check the text when people submit, or when they leave the textarea, and not while they're still writing it. Once an error shows, check again as they type, and clear it as soon as the text is right. The count is the exception, since it's how people keep within the limit as they write.
  • When a submit fails, move focus to the first invalid field, once the error is in the page, so keyboard and screen reader users land right on it and hear its error. As on the Input, a status message says what happened, since moving focus to a field that already has it doesn't announce anything.
  • Put novalidate on the form, so that the browser's own error bubbles don't compete with yours, and keep required on the textarea, which tells screen readers that it's required.

Adaptive

Pointer, touch and pen

  • Hover styles only apply to pointers that can actually hover (@media (hover: hover)), otherwise a tap would leave the border looking hovered.
  • On touch screens, the textarea is at least 44px tall, whatever its rows, and its text is at least 16px, whatever its size. Safari on iOS zooms in on a textarea with smaller text as soon as it gets focus, and people then have to zoom back out to see the rest of the form.
  • The resize handle is small, and hard to drag with a finger, so on touch screens we recommend giving the textarea enough rows, or autoGrow, rather than relying on it.

More contrast

When someone asks for more contrast (prefers-contrast: more), as with Increase contrast on macOS and iOS, the border is drawn in the text colour at full strength, rather than at 55%. The text, the placeholder and the count already pass 4.5:1, so they stay as they are.

Forced colours

Windows contrast themes replace every colour with the user's own, and drop backgrounds and shadows. The Textarea's border stays, since the system draws it in a colour of its own, focus is drawn with outline, and a disabled textarea is marked with GrayText. A read-only textarea's border stays dashed. The invalid border's second pixel is a shadow, and the count's red is a colour, which forced colours drop, so in these themes it's the words, in the error message and the count, that tell people something's wrong.

Colour scheme

The tokens use light-dark(), which follows color-scheme, so the Textarea adapts to your light and dark themes on its own. Setting color-scheme on your themes also lets the browser match the parts of the textarea it draws itself, like its scrollbar and its resize handle.

Text size and spacing

Heights are minimums, in lines and rem, so the field grows with its text instead of clipping it, at 200% text size (WCAG 1.4.4) and with wider line, letter and word spacing (WCAG 1.4.12). Each row is one line of text at the textarea's own line height, with the lh unit, so a textarea of three rows still shows three lines when people widen the line spacing. The text wraps within the width of the field, so it never scrolls sideways.

Languages and direction

The Textarea uses logical properties, and resizes on the block axis rather than the vertical one, so it works the same in right-to-left languages and in vertical writing. What people write needs a little help, though:

<div dir="rtl" lang="ar">
  <label htmlFor="message">الرسالة</label>
  <Textarea id="message" dir="auto" rows={3} />
</div>
  • People can write in either direction, so give a textarea dir="auto", which lands on the <textarea>, and lets it follow whatever they write, while the page around it keeps its own direction.
  • Translate the count along with labels, hints and error messages, with countLabel, and mark any text that's in another language with lang, so that screen readers pronounce it correctly (WCAG 3.1.2).

UX Checklist

RuleLevelScopeRequirementBasisCheck
textarea/native-elementMustComponentIt's a native <textarea>, and never a contenteditable element or a role="textbox"Open Components, beyond WCAG 2.1.1, 4.1.2 (A)Unit test
textarea/visible-labelMustUsageEvery textarea has a visible <label>, linked with for, rather than a placeholder aloneWCAG 1.3.1, 3.3.2, 4.1.2 (A)axe label
textarea/label-in-nameMustUsageAn aria-label or aria-labelledby includes the visible label, ideally at the startWCAG 2.5.3 (A)axe label-content-name-mismatch
textarea/describedShouldUsageHints, like what to include, are linked with aria-describedby, and a placeholder only ever shows a short exampleWCAG 1.3.1 (A), Open ComponentsScreen reader
textarea/keyboardMustComponentTab moves focus to and from it, and the browser's text editing keys work as they do in any textareaWCAG 2.1.1, 2.1.2 (A)Unit test
textarea/enter-new-lineMustBothEnter starts a new line and never submits the form, which has a submit buttonHTMLUnit test
textarea/allow-pasteMustBothPasting and autofill are never blockedOpen ComponentsUnit test, review
textarea/no-truncationMustBothA limit is counted rather than enforced with maxlength, so people can type and paste past itOpen ComponentsUnit test
textarea/limit-in-hintShouldUsageA limit is stated in the hint, linked with aria-describedby, before people start writingWCAG 3.3.2 (A)Screen reader
textarea/countShouldComponentThe count says how many characters are left, or how many too many, describes the textarea, and over the limit sets aria-invalid="true"WCAG 3.3.1 (A), Open ComponentsUnit test
textarea/announce-countShouldComponentThe count is announced through one polite live region once people pause near the limit, and never on every keystrokeWCAG 4.1.3 (AA), Open ComponentsUnit test, screen reader
textarea/input-purposeMustUsageA textarea that asks for the person's own address has autocomplete="street-address"WCAG 1.3.5 (AA)axe autocomplete-valid, review
textarea/error-messageMustUsageAn invalid textarea has an error message, in text and linked with aria-describedby, that says what's wrong and how to fix itWCAG 1.4.1, 3.3.1 (A), 3.3.3 (AA)Screen reader
textarea/invalid-stateMustBothA textarea sets aria-invalid="true" while its error message shows, and only looks invalid when it doesWCAG 4.1.2 (A), Open ComponentsUnit test
textarea/validate-lateShouldUsageErrors show when people submit or leave the textarea, never while they're first writing, and clear as soon as the text is fixedOpen ComponentsReview
textarea/focus-first-errorShouldUsageWhen a submit fails, focus moves to the first invalid field, or to a summary of the errors, once the error messages are in the pageOpen ComponentsKeyboard, screen reader
textarea/disabled-inertMustComponentA disabled textarea can't be focused, edited or submittedHTMLUnit test
textarea/read-only-valuesShouldUsageText people need to read but can't change is read-only rather than disabled, so they can focus it, scroll it and copy it, and its form sends itOpen ComponentsReview
textarea/keep-focusMustUsageA textarea is never disabled while it has focus, so while its form is sent, it's read-only insteadOpen ComponentsKeyboard
textarea/focus-not-obscuredMustUsageSticky headers and footers never cover a focused textarea, for example thanks to scroll-paddingWCAG 2.4.11 (AA), 2.4.12 (AAA)Keyboard
textarea/mirrors-in-rtlShouldBothIt uses logical properties, and a textarea for free text has dir="auto"Open ComponentsReview

FAQ

Textarea or something else?

A Textarea takes plain text over several lines. For anything else, another component will do a better job:

When people need to…Use
Type a single line of text, like a name or a subjectInput
Write several lines of plain text, like a message or a noteTextarea
Write a short answer that might run long, like a titleTextarea, with rows="1" and autoGrow
Format what they write, with bold text, lists or linksA rich text editor
Write code, with indenting and highlightingA code editor
Copyright © 2026