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)}
/>rows | Height |
|---|---|
| 1 or 2 | 86px. A min-height of 86px stops it going lower, so these look almost the same as 3. |
| 3 (default) | 88px |
| 4 | 110px |
| 6 | 154px |
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 state | Border | Background | Text |
|---|---|---|---|
| Default | 1px --color-grey-200 | --color-white | Placeholder in --color-text-disabled |
| Selected (focus) | 1px --color-primary-500 | --color-white | |
| Filled (has a value) | 1px --color-primary-500 | --color-white | 14px --color-text-primary |
| Error | 1px --color-red | --color-white | Message 12px medium --color-red, 4px below |
| Disabled | none | --color-grey-50 | Label 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
| Prop | Type | Default | Notes |
|---|---|---|---|
label | ReactNode | Rendered in a <label> tied to the textarea. Left out, there is no label. | |
placeholder | string | "Placeholder" | Pass your own, or "" for none. |
value | string | Controlled value. | |
onChange | (event) => void | Receives the native change event. | |
rows | number | 3 | Values under 3 barely change anything because of the 86px minimum height. |
error | boolean | false | Red border and aria-invalid. |
errorMessage | string | "" | Shown under the field only when error is true. |
disabled | boolean | false | |
size | "sm" | "md" | "md" | Padding only. |
prefixIcon, suffixIcon | Phosphor icon | 20px, top aligned. | |
clearable, onClear | As in Text input. | ||
className | string | Added 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 getsaria-invalid, and the message is linked witharia-describedbyand announced throughrole="alert". - A character count you add yourself is not linked to the field. If it matters, give the count an
idand passaria-describedbyto 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.