Skip to content

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.

Change the props and copy the code.

Markdown is not supported.

variant
color
size
resize
rows
label
Generated code
TSX
<Textarea
  label="Message"
  rows={4}
/>

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>

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>

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.

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.

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.

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>

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.

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.

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 :focus rather than :focus-visible, label over aria-label, the merged aria-describedby, the forced-colors border 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

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.