Design system

Text area

A labelled multi-line field for descriptions, briefs and instructions.

import { TextArea } from "@/ui";

Usage

TextArea works like Text input: it is controlled, and onChange receives the native change event.

const [brief, setBrief] = useState("");

<TextArea
  label="Campaign brief"
  placeholder="Who the campaign is for, the offer, and the pipeline it should create"
  value={brief}
  onChange={(e) => setBrief(e.target.value)}
/>

It fills the width of its container. People can drag the bottom edge to make it taller; it cannot be resized sideways.

TextArea matches the Figma Text Input (Multi-Line) component.

Always pass a placeholder. It defaults to the literal text "Placeholder". label has no default: leave it out and no label renders.

Height

rows sets the starting height and defaults to 3. Pick it from how much people usually write: 3 for a description, 5 or 6 for instructions or a brief. The field does not grow as people type; it scrolls.

<TextArea
  label="Instructions for this workflow"
  placeholder="Every Monday, pull last week's paid media spend by channel and flag any campaign with cost per opportunity above $1,800."
  rows={6}
  value={notes}
  onChange={(e) => setNotes(e.target.value)}
/>
rowsHeight
1 or 286px. A min-height of 86px stops it going lower, so these look almost the same as 3.
3 (default)88px
4110px
6154px

Each extra row adds 22px (one line at 14px text).

Character limits

There is no built-in counter or maxLength display. When a limit matters, such as a meta description, show the count yourself in a 12px --color-text-secondary line under the field, and switch to error once the text is over. Prefer this to maxLength, which cuts pasted text off without saying so.

const LIMIT = 160;
const over = text.length > LIMIT;

<TextArea
  label="Meta description"
  placeholder="One or two sentences on what the page offers"
  value={text}
  onChange={(e) => setText(e.target.value)}
  error={over}
  errorMessage={`Search engines cut descriptions after ${LIMIT} characters. Remove ${text.length - LIMIT}.`}
/>
{!over && (
  <p style={{ margin: "6px 0 0", fontSize: 12, color: "var(--color-text-secondary)" }}>
    {text.length} of {LIMIT} characters
  </p>
)}

States

<TextArea label="Default" placeholder="Describe the audience" />
<TextArea label="Filled" value="Mid-market SaaS accounts that attended the September webinar…" />
<TextArea label="Error" error errorMessage="Add a description so Sage knows when to use this block." />
<TextArea label="Disabled" value="Book a 20-minute pipeline review with our team." disabled />

The states match Text input exactly, including the blue border once filled:

Figma stateBorderBackgroundText
Default1px --color-grey-200--color-whitePlaceholder in --color-text-disabled
Selected (focus)1px --color-primary-500--color-white
Filled (has a value)1px --color-primary-500--color-white14px --color-text-primary
Error1px --color-red--color-whiteMessage 12px medium --color-red, 4px below
Disablednone--color-grey-50Label and value in --color-text-disabled. The resize handle is removed.

The field is at least 86px tall. Padding is 10px by 16px (8px by 16px for size="sm"), the corner radius is --radius-md (8px), and the label is 14px medium, 6px above the field. Like Text input it takes prefixIcon, suffixIcon and clearable; icons sit at the top.

Props

PropTypeDefaultNotes
labelReactNodeRendered in a <label> tied to the textarea. Left out, there is no label.
placeholderstring"Placeholder"Pass your own, or "" for none.
valuestringControlled value.
onChange(event) => voidReceives the native change event.
rowsnumber3Values under 3 barely change anything because of the 86px minimum height.
errorbooleanfalseRed border and aria-invalid.
errorMessagestring""Shown under the field only when error is true.
disabledbooleanfalse
size"sm" | "md""md"Padding only.
prefixIcon, suffixIconPhosphor icon20px, top aligned.
clearable, onClearAs in Text input.
classNamestringAdded to the wrapper <div>. Use for layout only.

Any other prop (onBlur, onKeyDown, name, maxLength, aria-*, ref) is passed to the <textarea>.

Accessibility

  • The <label> is linked to the textarea, so clicking it focuses the field and screen readers use it as the name.
  • With error, the textarea gets aria-invalid, and the message is linked with aria-describedby and announced through role="alert".
  • A character count you add yourself is not linked to the field. If it matters, give the count an id and pass aria-describedby to the TextArea.
  • Enter adds a new line. Do not make Enter submit the form from a TextArea; people expect to write more than one line.

Do and don't

  • Do use a TextArea when the answer is a sentence or more: descriptions, briefs, instructions for a workflow.
  • Do use the placeholder to show what a good answer looks like, in the reader's words.
  • Don't use a TextArea for a name, ID or address. Use Text input.
  • Don't use this field for chat with Sage. The chat composer is SageTextBox.

On this page