A multi-line Text field, with the same label, hint, error, changed state and length count.
Basic
Pass a label, and a hint if the label cannot say it all. The box is three lines tall and never shorter than 6rem. Enter adds a line, and the reader can drag the corner to make it taller. The placeholder shows an example and never replaces the label.
Example
Example
'use client';
import { TextArea } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function TextAreaBasic(): ReactElement {
return (
<TextArea
label="Short description"
description="One or two sentences, shown in search results and invites."
placeholder="A shared space for the Northwind design team"
style={{ width: '100%', maxWidth: '24rem' }}
/>
);
}States
The states match Text field, on the taller box.
- Changed: a light signal orange edge and a clay chip beside the label.
- Invalid: the error edge and an error line. Invalid outranks changed, so the chip steps aside.
- Disabled: recessed.
- Read-only: looks enabled and still submits.
Example
Example
'use client';
import { TextArea } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function TextAreaStates(): ReactElement {
return (
<div className="stack" style={{ width: '100%', maxWidth: '24rem' }}>
<TextArea
label="Welcome message"
defaultValue={'Welcome to Northwind.\nStart with the brief in the pinned note.'}
isChanged
/>
<TextArea
label="Invite note"
defaultValue="Hi!"
isInvalid
errorMessage="Write at least one full sentence."
/>
<TextArea label="Archived note" defaultValue="Kept for the 2025 audit." isDisabled />
<TextArea label="Plan terms" defaultValue="Billed yearly, cancel any time." isReadOnly />
</div>
);
}Length and height
Pass budget to count the value's length against a rough target. The count appears from 70% of the budget and never blocks typing. Pass rows for a taller box: the height is the rows times the line height, plus padding and edge, and never less than 6rem.
Example
Example
'use client';
import { TextArea } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function TextAreaCounter(): ReactElement {
return (
<TextArea
label="About the workspace"
description="Keep typing past 120. Nothing stops you."
defaultValue={
'A shared space for the Northwind design team.\nBriefs, reviews and the weekly notes live here.'
}
budget={120}
rows={5}
style={{ width: '100%', maxWidth: '24rem' }}
/>
);
}- Units: the count is in UTF-16 code units, as JavaScript and
maxLengthmeasure it. A line break counts as one, as in the valueonChangereceives. An emoji, or an accented letter built from two code points, can count as two. - Forms: a browser sends each line break as two characters, so a server measuring the request sees one more per line.
- Limits: the budget is a soft target the count can pass.
maxLengthis a hard stop the counter does not read, so setbudgetat or belowmaxLength.
To word the count another way, such as in another language, pass counter with a label function. It receives the count, the budget and whether the count is past it, and returns the text. counter.showFrom sets the share of the budget where the count first appears.
<TextArea
label="Kurzbeschreibung"
budget={60}
counter={{ label: ({ count, budget }) => `${count} von etwa ${budget}` }}
/>The count is announced each time it changes. If that is too often, build the field from its parts and pass FieldCounter a count that settles after a pause, as Field parts describes.
Usage
Every value works controlled and uncontrolled, and a field with a name submits in a plain form. onChange receives the string, with each line break as \n.
import { useState } from 'react';
import { TextArea } from '@snapshot-ui/react';
export function ShortDescription({ saved }: { saved: string }) {
const [value, setValue] = useState(saved);
return (
<TextArea
label="Short description"
description="One or two sentences, for search results."
name="description"
value={value}
onChange={setValue}
isChanged={value !== saved}
budget={120}
rows={4}
/>
);
}The types require an accessible name: a label, an aria-label when no label is shown, or an aria-labelledby that points at a label elsewhere.
For a custom layout, use Snapshot UI's Label, Description and FieldError inside React Aria's TextField, with React Aria's TextArea and the classes input textarea. There is no bare textarea part, since those classes are all it would add. Field parts has the layout.
Props
Every React Aria TextField prop is accepted except type and pattern, which belong to a single line. These are the ones Snapshot UI adds or shapes.
budget number | About how long the value should be. Shows a length count from 70% of the
budget. It never blocks typing, so the count can pass it. The length is in
UTF-16 code units, as |
|---|---|
changedLabel string | The text of the Changed chip. Default: 'Changed' |
counter Pick<FieldCounterProps, "label" | "showFrom"> | Options for the |
defaultValue string | The default value (uncontrolled). |
description ReactNode | A hint under the control. It becomes the control's accessible description. |
errorMessage ((validation: ValidationResult) => ReactNode) | ReactNode | The error line, shown only while the field is invalid. A function receives the validation result. Without it the field shows its own validation messages. |
isChanged boolean | Whether the value differs from the saved one. Shows the clay Changed chip and the light signal orange border, and a screen reader reads the chip with the field. An invalid field shows its error instead. |
isDisabled boolean | Whether the input is disabled. |
isInvalid boolean | Whether the value is invalid. |
isReadOnly boolean | Whether the input can be selected but not changed by the user. |
isRequired boolean | Whether user input is required on the input before form submission. |
label ReactNode | The field's name, shown above the control. Required unless the field has an |
name string | The name of the input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname). |
onChange ((value: string) => void) | Handler that is called when the value changes. |
placeholder string | Example text shown while the control is empty. It never replaces the label. |
rows number | How many lines tall the control is. It is never shorter than 6rem (three lines and their padding), so fewer rows look the same. The reader can drag the corner to resize it. Default: 3 |
textAreaRef Ref<HTMLTextAreaElement> | A ref to the textarea element. |
validate ((value: string) => ValidationError | true | null | undefined) | A function that returns an error message if a given value is invalid.
Validation errors are displayed to the user when the form is submitted
if |
value string | The current value (controlled). |
Accessibility
A native textarea, labelled by the label and described by the hint and error line, with React Aria handling the wiring. While the field is changed and valid, a screen reader reads the Changed chip after the hint. An invalid field reads its hint, then its error line.
| Tab | Moves focus into the field. The edge firms to 1.5px ink and the 2px ring sits outside it. |
|---|---|
| ShiftTab | Moves focus back out. |
| Enter | Adds a new line. It never submits the form. |
- Resizing: the corner handle works with a pointer, in browsers that draw one. The keyboard cannot resize, so set
rowsto the height the content needs. - Placeholder: taupe and quiet by design, so it never carries anything the label does not.
- Count: a polite status region.
- Missing name: the field does not compile. TypeScript either names only
aria-labelledbyor says the props are not assignable. Both mean the field has no accessible name.
Design rules
What keeps fields in the system.
Name the field in the label, in sentence case. Use the hint for what the label cannot say.
Use a text area for text that runs to a sentence or more. A name, an email or a title is a text field.
Set rows to the length you expect, so the box starts at the right height.
Never use the placeholder as the label.
Never block typing at the budget. The count advises.
Never leave the chip beside an error. Invalid outranks changed.