Textarea

Developer Experience (DX)

How developers use the Textarea, with the Input's consistent, typed API that works with your forms however you build them.

Next, let's look at how developers use the Textarea. It has the Input's API, with the same names as every other component, it's typed end to end, and it works with your forms however you build them.

import { useState } from "react";
import { Textarea } from "@/components/Textarea";

export function Profile() {
  const [bio, setBio] = useState("");

  return (
    <>
      <label htmlFor="bio">Bio</label>
      <p id="bio-hint">You can enter up to 160 characters</p>
      <Textarea
        id="bio"
        limit={160}
        aria-describedby="bio-hint"
        value={bio}
        onChange={(event) => setBio(event.target.value)}
      />
    </>
  );
}

Consistent

Once you've learned the Input's API, you already know most of the Textarea's. These names mean the same thing everywhere in Open Components:

NameKindWhat it means
sizePropxs, sm, md, lg or xl, with the same text and padding everywhere
disabledPropThe component can't be used
valuePropThe value, which you bind the way your framework binds a form control's

The rest are the Textarea's own: rows, the native attribute, autoGrow and maxRows for its height, and limit and countLabel for its count. We recommend size="md" and rows="3" as the defaults. The Textarea also leaves a few props out, on purpose:

  • There's no variant or color, for the same reasons as the Input: the fields in a form are all equally important, a field without a border would be invisible when it's empty, and its one coloured state, invalid, comes from aria-invalid.
  • There's no label, hint or error. Each of them is an element of its own, linked by ID, which the Field on our Roadmap will render.
  • There's no maxlength either. limit takes its place, since it counts without cutting anything off. The native attribute still lands on the <textarea> if you pass it, but we recommend against it (see Don't cut people off).
  • There's no resize. A textarea people resize on the block axis, or one that grows on its own, covers what people need, and turning resizing off without growing leaves them stuck with whatever height you picked. The one exception is a disabled textarea, which has no resize handle, since nobody can write in it then.
  • There are no leading, trailing or actions slots. A textarea holds plain text over several lines, so an icon or a button inside it would sit in the way of what people write. Put buttons, like Send, after it in the form.

Type-safe

Props and events are typed, so your editor can autocomplete them and your type checker (tsc, vue-tsc, svelte-check, ngc or astro check) catches mistakes like rows="three" or size="medium".

PropTypeDefaultDescription
size"xs" | "sm" | "md" | "lg" | "xl""md"How big its text and padding are
rowsnumber3How many lines it shows, and is never shorter than
autoGrowbooleanfalseGrows with its text instead of scrolling, and can't be resized
maxRowsnumberHow many lines it grows to with autoGrow, before it scrolls
limitnumberThe most characters it takes, which shows a count and never cuts people off
countLabel(remaining: number) => stringThe count's text, for the characters left, which are negative over the limit
disabledbooleanfalseBlocks focus, editing and submission
valuestringThe value, when you bind it. Without it, the textarea keeps its own
EventPayloadDescription
inputEventFires as people type
changeEventFires when people commit a change, like when they leave the textarea

Both come straight from the <textarea>, like every other native event, such as focus and blur, and a binding to value listens to them for you. The Textarea has no slots.

We also export TextareaProps, which comes in handy for wrappers and data-driven forms:

import type { TextareaProps } from "@/components/Textarea";

const fields: (TextareaProps & { name: string; label: string })[] = [
  { name: "bio", label: "Bio", limit: 160 },
  { name: "address", label: "Street address", rows: 3 },
];

Composable

The Textarea renders two elements, so it splits what you pass between them, just like the Input. Classes and styles go on the root, so a class can set its variables and its width. Everything else lands on the <textarea>, including id, name, placeholder, required, readonly, autocomplete, spellcheck, dir, ARIA attributes and event listeners. A ref to the Textarea reaches the <textarea> too, since that's what form libraries and focus() need.

Forms

The <textarea> is a real form control, so the Textarea works with your forms however you build them. You can bind its value, as above, or leave it to the browser and read it from the form when it's sent, by its name. That also means the form works before your JavaScript loads.

import type { FormEvent } from "react";

export function Support() {
  function send(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    api.send(data.get("subject"), data.get("message"));
  }

  return (
    <form onSubmit={send}>
      <label htmlFor="subject">Subject</label>
      <Input id="subject" name="subject" />
      <label htmlFor="message">Message</label>
      <Textarea id="message" name="message" rows={5} />
      <Button type="submit" color="primary">Send</Button>
    </form>
  );
}

Form libraries work the same way. They register the <textarea> itself, through its ref or its attributes and listeners, which is why the Textarea puts them all on the <textarea>.

With other components

  • The Input takes a single line, and the Textarea several, with the same sizes, padding, border and states, so the two sit side by side in a form.
  • The Field, on our Roadmap, will render the label, hint and error message around a Textarea, give it an id, and set its aria-describedby and aria-invalid for you, keeping the count after them.
  • A Button after the textarea sends the form. It's type="submit", since Enter in a textarea starts a new line rather than submitting.
  • The count announces itself through the same announce as the Button's loading state, so the whole page shares one live region.

Controllable

The Textarea doesn't keep any state you can't reach. The value is yours to bind, or to leave to the browser, and every other state is a prop or an attribute that you control:

StateHow you control it
Valuevalue, bound to your state, or left to the browser
Heightrows, autoGrow and maxRows
Disableddisabled
Read-onlyreadonly, which lands on the <textarea>
Invalidaria-invalid, set while its error message shows, or by the Textarea over its limit
FocusA ref to its <textarea>, and focus()

DX Checklist

RuleLevelScopeRequirementBasisCheck
textarea/shared-vocabularyMustComponentIt uses the shared names size, disabled and value, with the Input's sizesOpen ComponentsReview
textarea/typedMustComponentProps and events are typed, with unions rather than string, and rows, maxRows and limit take numbersOpen ComponentsType check
textarea/control-attributesMustComponentAttributes, listeners and refs land on the <textarea>, while classes and styles land on the rootOpen ComponentsUnit test
textarea/form-controlMustComponentIts form sends its text under its name, whether the value is bound or notHTMLUnit test
textarea/controllableShouldComponentIts value can be bound or left to the browser, and every other state is a prop or an attributeOpen ComponentsUnit test

FAQ

Copyright © 2026