Textarea

User Interface (UI)

How a textarea looks, from its parts and sizes to how tall it gets, its count, its states and its tokens.

Let's start with how a textarea looks: its parts, its sizes, how tall it is, how it counts what people write and how it shows each of its states.

Anatomy

  1. Container — The root <div>, which carries the size and holds the count below the field.
  2. Control — The <textarea> itself, which draws the field, holds the text and takes focus. Its label names it.
  3. Resize handle — The browser's own, in the corner, which people drag to make the field taller or shorter.
  4. Count — Optional. How many characters are left, or how many too many, which describes the control.
  5. Focus ring — An outline around the field, drawn outside it so nothing moves.

Every textarea has a container and a control. Unlike the Input, the control draws the field itself, since its resize handle and its scrollbar belong to it. The label, any hint and any error message sit outside it, in the page, and point to the control by its id (see Label every textarea). The parts always render in the same order, which you can see in the DOM contract.

Sizes

The size prop scales the text, padding and corner radius together, and md is the default. They're the same as the Input's, so a textarea and an input of the same size line up in a form, and a textarea of one row is exactly as tall as an input.

<label htmlFor="subject-xs">Subject</label>
<Input id="subject-xs" size="xs" />
<label htmlFor="message-xs">Message</label>
<Textarea id="message-xs" size="xs" rows={1} />
{/* …sm, md, lg, xl */}
SizeOne rowTextLine heightPaddingRadius
xs24px12px18px8px6px
sm32px14px21px10px6px
md36px14px21px12px8px
lg40px16px24px14px8px
xl48px16px24px16px12px

Sizes are set in rem, and the table shows the values we recommend at the default text size of 16px. The line height is 1.5 times the text, a little more than the Input's, since people read what they've written back over several lines. The padding above and below the text is whatever's left of one row's height, so every size meets the 24 × 24px minimum from WCAG 2.5.8. On touch screens, the text grows to at least 16px, since Safari on iOS zooms in on a textarea with smaller text as soon as it gets focus, and the field is at least 44px tall, whatever its rows.

Height

A textarea's height says how much people are expected to write, just like an input's width. rows sets how many lines it shows, and it's never shorter than that, so it's the first thing to pick. From there, the height can go one of two ways. Either people resize it, by dragging the handle in its corner, or it grows with its text, with autoGrow, up to maxRows.

<label htmlFor="address">Street address</label>
<Textarea id="address" rows={3} autoComplete="street-address" />

<label htmlFor="comment">Comment</label>
<Textarea id="comment" rows={2} autoGrow maxRows={6} />
  • Pick rows for the text you expect, so people can see all of it as they write. Three rows suit a short note, while a message or a description might need five or more. A box that's too small makes people scroll inside it to check what they wrote, which is hard for everyone and harder still at 200% zoom.
  • A resizable textarea only resizes on the block axis, taller or shorter, since a textarea that gets wider than its column breaks the layout around it (WCAG 1.4.10). It never gets shorter than its rows.
  • An auto-growing textarea has no handle, since there's nothing left for people to resize. It grows with field-sizing: content where the browser supports it, and elsewhere the Textarea sets its height from a script as people type.
  • Give an auto-growing textarea a maxRows, so that a long text doesn't push the rest of the form, and its submit button, off the screen. Past it, the text scrolls inside the field.
  • We recommend letting it fill the width of its column, since lines that run the whole width of a wide screen are hard to read back, and a narrow textarea wraps them short. Set its width from a class if the column is wider than 80 characters, which is as wide as a block of text should get (WCAG 1.4.8).

Character count

When there's a limit on how much people can write, like a bio for a profile or a message to a support team, limit shows a count under the field. It says how many characters are left, and once people go over, how many too many. We recommend saying what the limit is in the hint too, so people know it before they start.

You can enter up to 160 characters

18 characters left
<label htmlFor="bio">Bio</label>
<p id="bio-hint">You can enter up to 160 characters</p>
<Textarea id="bio" limit={160} aria-describedby="bio-hint" />
  • The count never stops people typing or pasting, unlike maxlength. Over the limit, it turns red, says by how much, and marks the textarea invalid, so people can decide what to cut (see Don't cut people off).
  • The count describes the textarea, after any hint, so screen readers read it when the textarea gets focus. As people near the limit, it's also announced once they pause typing, rather than on every keystroke.
  • The count is named after the textarea's id, as in bio-count, so give every textarea with a limit an id, which its label needs anyway.
  • The count's text is in English. To write it in the page's language, pass countLabel, which gets the number of characters left, and a negative number over the limit.

States

Each state has a look of its own, styled from the attribute on the <textarea> that exposes it. That way, what people see and what assistive technologies report can never drift apart, and a textarea can't look invalid without actually being invalid.

At rest (try hovering, clicking or tabbing to it)
Disabled
Read-only

Enter a reason for returning the item

Invalid
<label htmlFor="message">Message</label>
<Textarea id="message" rows={2} defaultValue="Could we move our call to Friday?" />

<label htmlFor="notes">Notes</label>
<Textarea id="notes" rows={2} defaultValue="Call back after 3pm" disabled />

<label htmlFor="terms">Terms</label>
<Textarea id="terms" rows={2} defaultValue="Payment is due within 30 days." readOnly />

<label htmlFor="reason">Reason for return</label>
<Textarea id="reason" rows={2} aria-invalid="true" aria-describedby="reason-error" />
<p id="reason-error">Enter a reason for returning the item</p>
StateSelectorRecommended look
Hover:hover, inside @media (hover: hover)The border at 80% of the text colour, up from 55%
Focus:focus-visibleA 2px outline, 2px from the edge, in --color--focus
Disabled:disabled50% opacity, for the count too, a not-allowed cursor and no resize handle
Read-only[readonly]A dashed border, with the text at full contrast, and the default cursor
Invalid[aria-invalid="true"]A 2px border in --color--error-text

The <textarea> draws the field, so it reads each state from its own attributes, and the root only dims the count along with it while it's disabled. We recommend leaving :invalid and :user-invalid out of it. The browser decides them from its own checks, like required, so :invalid matches an empty required textarea before anyone has written in it, and neither of them knows what your error message says. The invalid look comes from aria-invalid instead, which you set when the error message shows, and which the Textarea sets itself over its limit.

None of the states change the textarea's size or position. The invalid border's second pixel is drawn inside the field, and the focus ring outside it. You'll find how each state behaves under Every state.

Tokens

The textarea reads three theme tokens, which every component shares:

TokenUsed for
--color--neutral-textThe text, and mixed into the border, the placeholder and the count
--color--error-textThe border of an invalid textarea, and the count over its limit
--color--focusThe focus ring, which is the same for every component

There's no color prop, so the textarea doesn't read any of the other colours. Everything else is mixed from the text colour, as on the Input: 55% for the border, which needs 3:1 against the page, 80% for the border on hover, and 70% for the placeholder and the count, which need 4.5:1. With our reference tokens, from the Tailwind CSS palette, the border comes to 3.5:1 and the placeholder to 5.6:1 on white, and 5.0:1 and 7.4:1 on zinc-900. If you change the tokens, make sure to check the same pairs. You'll find the tokens themselves in the Button's tokens.css, which you can copy into your theme.

The Textarea's own variables are set on its root:

VariableWhat it sets
--textarea--heightThe height of one row, the same as the Input's, which sets the padding
--textarea--paddingThe padding before and after the text
--textarea--padding-blockThe padding above and below the text, from the height of one row
--textarea--font-sizeThe text, and the count
--textarea--radiusThe corners
--textarea--border-colorThe border, which hover, invalid and more contrast change
--textarea--min-heightThe height of rows lines, which resizing and growing never go below
--textarea--max-heightThe height of maxRows lines, which growing stops at
--textarea--count--colorThe count, until it's over the limit
--textarea--rows, --textarea--max-rowsrows and maxRows, which the Textarea sets itself, so set the props rather than these

If you need to tune a textarea, you can set any of the others from a class, like --textarea--radius or --textarea--min-height. The Textarea's styles sit in the components cascade layer, so your class wins without any specificity tricks:

Feedback.tsx
import styles from "./Feedback.module.css";

export function Feedback() {
  return (
    <>
      <label htmlFor="feedback">Feedback</label>
      <Textarea id="feedback" className={styles.soft} />
    </>
  );
}
Feedback.module.css
.soft {
  --textarea--radius: 1rem;
}

UI Checklist

RuleLevelScopeRequirementBasisCheck
textarea/contrastMustComponentThe text, the placeholder and the count have a contrast of 4.5:1, and the focus ring has 3:1 against the pageWCAG 1.4.3, 1.4.11 (AA)axe color-contrast, contrast checker
textarea/boundary-contrastMustComponentThe border has a contrast of 3:1 against the page, so an empty textarea stays visibleWCAG 1.4.11 (AA)Contrast checker
textarea/focus-visibleMustComponentFocus shows an outline at least 2px thick around the field, outside its borderWCAG 2.4.7 (AA), 2.4.13 (AAA)Keyboard
textarea/target-sizeMustComponentThe field is at least 24px tall, and at least 44px tall on touch screensWCAG 2.5.8 (AA), 2.5.5 (AAA)Emulation
textarea/no-zoom-on-focusShouldComponentOn touch screens, the text is at least 16px, so Safari on iOS doesn't zoom in when the textarea gets focusOpen ComponentsEmulation
textarea/min-rowsMustComponentIt's never shorter than its rows, whether people resize it or it grows on its ownOpen ComponentsVisual regression test
textarea/resizableMustComponentIts height is a minimum, in lines of its own line height plus padding in rem, so it grows with its text instead of clipping itOpen Components, beyond WCAG 1.4.4, 1.4.12 (AA)200% zoom
textarea/block-resizeShouldComponentPeople resize it on the block axis only, and resizing is only turned off when it grows on its own or is disabledOpen ComponentsVisual regression test
textarea/auto-growShouldComponentWith autoGrow, it grows with its text up to maxRows, then scrolls, with field-sizing: content or a script where that's not supportedOpen ComponentsVisual regression test, 200% zoom
textarea/stable-sizeMustComponentNo state changes the field's size or positionOpen ComponentsVisual regression test
textarea/state-selectorsShouldComponentStates are styled from the attributes on the <textarea> that expose them, and never from :invalid or :user-invalidOpen ComponentsReview
textarea/hover-capableShouldComponentHover styles only apply inside @media (hover: hover)Open ComponentsReview
textarea/forced-colorsMustComponentThe border, and the focus, disabled and read-only states, stay visible in forced colours modeOpen ComponentsEmulation
textarea/more-contrastShouldComponentThe border is drawn at full strength when prefers-contrast: more is setOpen ComponentsEmulation
textarea/fits-textShouldUsageIts rows fit the text it expects, and it's never wider than its columnOpen Components, beyond WCAG 1.4.10 (AA)Review

FAQ

Copyright © 2026