User Experience (UX)
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 withforand the textarea'sid(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'sid. The Textarea adds its count after it. - If you name a textarea with
aria-labeloraria-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
idfor 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
| Key | Textarea |
|---|---|
Tab, Shift+Tab | Move focus to and from the textarea |
Arrows, Home and End | Move the caret, line by line and within a line |
Enter | Starts 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.
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 pointaria-describedbyat 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
novalidateon the form, so that the browser's own error bubbles don't compete with yours, and keeprequiredon 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 withlang, so that screen readers pronounce it correctly (WCAG 3.1.2).
UX Checklist
| Rule | Level | Scope | Requirement | Basis | Check |
|---|---|---|---|---|---|
textarea/native-element | Must | Component | It'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-label | Must | Usage | Every textarea has a visible <label>, linked with for, rather than a placeholder alone | WCAG 1.3.1, 3.3.2, 4.1.2 (A) | axe label |
textarea/label-in-name | Must | Usage | An aria-label or aria-labelledby includes the visible label, ideally at the start | WCAG 2.5.3 (A) | axe label-content-name-mismatch |
textarea/described | Should | Usage | Hints, like what to include, are linked with aria-describedby, and a placeholder only ever shows a short example | WCAG 1.3.1 (A), Open Components | Screen reader |
textarea/keyboard | Must | Component | Tab moves focus to and from it, and the browser's text editing keys work as they do in any textarea | WCAG 2.1.1, 2.1.2 (A) | Unit test |
textarea/enter-new-line | Must | Both | Enter starts a new line and never submits the form, which has a submit button | HTML | Unit test |
textarea/allow-paste | Must | Both | Pasting and autofill are never blocked | Open Components | Unit test, review |
textarea/no-truncation | Must | Both | A limit is counted rather than enforced with maxlength, so people can type and paste past it | Open Components | Unit test |
textarea/limit-in-hint | Should | Usage | A limit is stated in the hint, linked with aria-describedby, before people start writing | WCAG 3.3.2 (A) | Screen reader |
textarea/count | Should | Component | The 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 Components | Unit test |
textarea/announce-count | Should | Component | The count is announced through one polite live region once people pause near the limit, and never on every keystroke | WCAG 4.1.3 (AA), Open Components | Unit test, screen reader |
textarea/input-purpose | Must | Usage | A textarea that asks for the person's own address has autocomplete="street-address" | WCAG 1.3.5 (AA) | axe autocomplete-valid, review |
textarea/error-message | Must | Usage | An invalid textarea has an error message, in text and linked with aria-describedby, that says what's wrong and how to fix it | WCAG 1.4.1, 3.3.1 (A), 3.3.3 (AA) | Screen reader |
textarea/invalid-state | Must | Both | A textarea sets aria-invalid="true" while its error message shows, and only looks invalid when it does | WCAG 4.1.2 (A), Open Components | Unit test |
textarea/validate-late | Should | Usage | Errors show when people submit or leave the textarea, never while they're first writing, and clear as soon as the text is fixed | Open Components | Review |
textarea/focus-first-error | Should | Usage | When a submit fails, focus moves to the first invalid field, or to a summary of the errors, once the error messages are in the page | Open Components | Keyboard, screen reader |
textarea/disabled-inert | Must | Component | A disabled textarea can't be focused, edited or submitted | HTML | Unit test |
textarea/read-only-values | Should | Usage | Text 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 it | Open Components | Review |
textarea/keep-focus | Must | Usage | A textarea is never disabled while it has focus, so while its form is sent, it's read-only instead | Open Components | Keyboard |
textarea/focus-not-obscured | Must | Usage | Sticky headers and footers never cover a focused textarea, for example thanks to scroll-padding | WCAG 2.4.11 (AA), 2.4.12 (AAA) | Keyboard |
textarea/mirrors-in-rtl | Should | Both | It uses logical properties, and a textarea for free text has dir="auto" | Open Components | Review |
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 subject | Input |
| Write several lines of plain text, like a message or a note | Textarea |
| Write a short answer that might run long, like a title | Textarea, with rows="1" and autoGrow |
| Format what they write, with bold text, lists or links | A rich text editor |
| Write code, with indenting and highlighting | A code editor |
maxlength, pasting a long text keeps the start and silently drops the end, so people may send something unfinished without knowing. Letting them go over, and saying by how much, lets them choose what to cut, which is also how GOV.UK's character count works.Enter for a new line out of habit, everywhere else, and a message sent half-written can't be taken back. If your users expect it, like in a chat they use all day, let them choose it in a setting, keep Shift+Enter for a new line, and keep a Send button that people can reach with Tab.