Textarea
Input with more than one line. Variants, color roles, sizes, the fixed width, unstyled and the whole label set behave exactly as they do on Input — this page covers only where the two differ.
- Stable
- React 19
- Native <textarea>
- Shares field.css with Input
Markdown is not supported.
Code
<Textarea label="Message" rows={4} /><div class="wayra-label" data-placement="top" data-size="md"><label class="wayra-label-text" for="usage-msg">Message</label><textarea class="wayra-textarea" id="usage-msg" rows="4"></textarea></div>Everything Input documents applies here: the four variants, the eight roles, the label, helper and error contract, validation and read-only over disabled. The rest of this page is only the delta.
Playground
Section titled “Playground”Change the props and copy the code.
Markdown is not supported.
Generated code
<Textarea
label="Message"
rows={4}
/>Variants
Section titled “Variants”The same four Input ships, with the same meanings. See Input’s variants for what each one is for.
Code
<Textarea aria-label="outline" /><Textarea variant="filled" aria-label="filled" /><Textarea variant="soft" aria-label="soft" /><Textarea variant="ghost" aria-label="ghost" /><textarea class="wayra-textarea" data-variant="outline" rows="3" placeholder="outline" aria-label="outline"></textarea><textarea class="wayra-textarea" data-variant="filled" rows="3" placeholder="filled" aria-label="filled"></textarea><textarea class="wayra-textarea" data-variant="soft" rows="3" placeholder="soft" aria-label="soft"></textarea><textarea class="wayra-textarea" data-variant="ghost" rows="3" placeholder="ghost" aria-label="ghost"></textarea>Colors
Section titled “Colors”The same eight roles, drawn the same way: the edge at full strength on
outline/soft/ghost, the surface on filled.
Four of the eight roles. See Input for all of them.
Code
<Textarea color="primary" aria-label="primary" /><Textarea color="secondary" aria-label="secondary" /><Textarea color="accent" aria-label="accent" /><Textarea color="neutral" aria-label="neutral" /><textarea class="wayra-textarea" data-color="primary" rows="3" placeholder="primary" aria-label="primary"></textarea><textarea class="wayra-textarea" data-color="secondary" rows="3" placeholder="secondary" aria-label="secondary"></textarea><textarea class="wayra-textarea" data-color="accent" rows="3" placeholder="accent" aria-label="accent"></textarea><textarea class="wayra-textarea" data-color="neutral" rows="3" placeholder="neutral" aria-label="neutral"></textarea>Rows control height
Section titled “Rows control height”This is the first real difference. rows sets the height, and the size step sets a
min-height floor instead. The floor matches the height an Input of that step would
have, so a short textarea still lines up with the fields around it and a drag cannot shrink
it below one step.
Code
<Textarea rows={2} aria-label="rows=2" /><Textarea rows={4} aria-label="rows=4" /><Textarea rows={8} aria-label="rows=8" /><textarea class="wayra-textarea" rows="2" placeholder="rows=2" aria-label="rows=2"></textarea><textarea class="wayra-textarea" rows="4" placeholder="rows=4" aria-label="rows=4"></textarea><textarea class="wayra-textarea" rows="8" placeholder="rows=8" aria-label="rows=8"></textarea>line-height is 1.5 here and nowhere else in the library. This is the one field holding
prose, and every vertical measurement around it is derived from that number, including
where a floating label sits.
Resize
Section titled “Resize”Code
<Textarea aria-label="vertical" /><Textarea resize="none" aria-label="none" /><Textarea resize="both" aria-label="both" /><textarea class="wayra-textarea" data-resize="vertical" rows="3" placeholder="vertical" aria-label="vertical"></textarea><textarea class="wayra-textarea" data-resize="none" rows="3" placeholder="none" aria-label="none"></textarea><textarea class="wayra-textarea" data-resize="both" rows="3" placeholder="both" aria-label="both"></textarea>resize |
What it does |
|---|---|
vertical |
default — the useful half of the browser’s own handle |
none |
locks the field to rows |
both |
opt-in; nothing caps the horizontal drag, so the user can pull the field wider than its container |
horizontal is deliberately absent. A disabled field drops the handle entirely, because a
field nobody can type in should not offer one.
What’s not included
Section titled “What’s not included”No cols prop, and no width prop
Section titled “No cols prop, and no width prop”cols sizes the field in characters, and the inline axis is not up for negotiation: a
textarea is width: 100%, the same declaration every other field takes from field.css.
A character count would be sizing that never takes, which is also why Input drops the HTML
size attribute — see Width.
No pill shape
Section titled “No pill shape”On a box several lines tall a max radius becomes a lozenge whose curve eats the inline
space of the first and last lines, and there is nowhere for that text to go. rounded and
rectangle are the two values.
Code
<Textarea aria-label="rounded" /><Textarea shape="rectangle" aria-label="rectangle" /><textarea class="wayra-textarea" data-shape="rounded" rows="3" placeholder="rounded" aria-label="rounded"></textarea><textarea class="wayra-textarea" data-shape="rectangle" rows="3" placeholder="rectangle" aria-label="rectangle"></textarea>Label, helper, and error
Section titled “Label, helper, and error”The wrapper is the one Input documents, with
one change: left and floating place the label against the field’s first line rather
than its vertical center. A label pointing at the middle of an eight-row box points at
nothing.
The label sits against the first line, not the middle of the box.
Code
<Textarea label="Notes" labelPlacement="left" rows={4} /><div class="wayra-label" data-placement="left" data-size="md"><label class="wayra-label-text" for="ta-left">Notes</label><textarea class="wayra-textarea" id="ta-left" rows="4"></textarea></div>That offset is the one piece of textarea-specific CSS inside the shared .wayra-label
wrapper. It adds up the border, the top padding, and the extra space a 1.5 line-height
leaves above its own text.
Nothing to configure. name, ref, value, defaultValue and onChange all come from
ComponentProps<"textarea">, so a native form, TanStack Form or Formik work with no
adapter. The integration point is error plus aria-invalid.
react-hook-form is built in, exactly as it is on
Input. Inside Form — which provides the useForm instance
you own — a Textarea registers itself on name alone: no register spread, the validation
message arrives in the hint slot, and the field disables itself while the form submits.
An explicit error or disabled still wins, and noRhf turns the whole thing off for one
instance. Outside a Form, spread register("bio") onto the field and it binds the same
way — the Textarea composes the handlers and merges the ref for you, so there is no prop to
pass. useRHFIntegration — the hook the Textarea calls internally — wires anything else by
hand.
const schema = z.object({ bio: z.string().min(1, "Tell us something.") });
const form = useForm({ resolver: zodResolver(schema) });
<Form form={form} onSubmit={save}> <Textarea name="bio" label="Bio" rows={4} /> <Button type="submit">Save</Button></Form>The schema library is yours to pick — zod here, but any resolver react-hook-form
accepts works the same. The Textarea never sees it.
API reference
Section titled “API reference”TextareaProps extends ComponentProps<"textarea"> minus cols and color, which are
redefined or dropped below.
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"outline" | "filled" | "soft" | "ghost" |
"outline" |
As on Input. |
color |
WayraColor |
"primary" |
Edge on outline/soft/ghost, surface on filled. |
size |
"xs" | "sm" | "md" | "lg" | "xl" |
"md" |
Font size, padding and the min-height floor, not the height. |
shape |
"rounded" | "rectangle" |
"rounded" |
No pill. |
rows |
number |
— | Native. How tall the field starts. |
resize |
"vertical" | "none" | "both" |
"vertical" |
See Resize. |
label |
ReactNode |
— | Wraps the field in the label contract. |
labelPlacement |
"top" | "left" | "floating" |
"top" |
left and floating align to the first line. |
helper |
ReactNode |
— | Persistent hint, wired into aria-describedby. |
error |
ReactNode |
— | Takes the hint slot, adds role="alert", sets aria-invalid. |
unstyled |
boolean |
false |
Drops the classes, keeps the data attributes. |
Everything else is the native attribute, typed: name, value, defaultValue,
onChange, ref, maxLength, required and the rest.
Accessibility
Section titled “Accessibility”| Key | Behavior |
|---|---|
Tab / Shift+Tab |
Moves focus. Skips disabled, stops on readOnly. |
Enter |
Inserts a newline. It never submits the form, unlike a text Input. |
| Text editing | Entirely native, including selection and undo. |
- Everything Input documents holds here, because both controls share the same wrapper
and the same field styles. See Input’s accessibility
notes for the ring on
:focusrather than:focus-visible,labeloveraria-label, the mergedaria-describedby, theforced-colorsborder rescue, and the measured contrast numbers. - The resize handle is not a control. It has no keyboard equivalent, so the field must
be usable at its starting
rows. Never rely on the user dragging it. - A disabled field drops the handle, so nothing offers an affordance that does nothing.
Do and don’t
Section titled “Do and don’t”Do
Four rows for a paragraph-length answer. The height is a promise about how much you expect.
Don't
One row for the same answer. The user types into a slot and cannot see what they wrote.
Do
vertical, the default. The user can make room without breaking the layout.
Don't
both on a block field. Nothing caps the horizontal drag, so the field can be pulled wider than the layout that holds it.