Textarea

A textarea is a multi-line text field. It has the same states as Input, and can be resized up and down.

Write at least 20 characters.

import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";

export default function TextareaDemo() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-4">
      <Textarea aria-label="Default" placeholder="Default" />
      <div className="flex flex-col gap-2">
        <Label htmlFor="bio">Bio</Label>
        <Textarea id="bio" placeholder="A sentence or two" />
      </div>
      <Textarea aria-label="Disabled" placeholder="Disabled" disabled />
      <div className="flex flex-col gap-2">
        <Textarea
          aria-label="Message"
          aria-invalid
          aria-describedby="message-error"
          defaultValue="Hi"
        />
        <p id="message-error" className="text-sm text-danger">
          Write at least 20 characters.
        </p>
      </div>
    </div>
  );
}

The code

The component is a single file, read from the repository when the site is built. Press Show code to see all of it.

import type { ComponentProps } from "react";
import { cx } from "@/lib/cx";
import { controlStyles } from "@/lib/styles";

// A multi-line text field. It is the native element with the same look as Input, and it can be
// resized up and down (not sideways, so it can't break the layout). Inside a Field, render it
// through FieldControl so the label, hint and error are connected to it.
export function Textarea({ className, ...props }: ComponentProps<"textarea">) {
  return <textarea className={cx("min-h-24 resize-y py-2", controlStyles, className)} {...props} />;
}

Props

PropTypeDefaultWhat it does
rowsnumber—How many lines tall it starts, if you don't want the default minimum.
placeholderstring—A hint shown while the field is empty. It is not a name for the field.
disabledbooleanfalseTurns the field off. It can't be focused or edited.
aria-invalidboolean—Marks the value as wrong, and turns the border to the danger color.
classNamestring—Extra classes, added after the textarea's own.

Textarea is the native element, so every other textarea prop works.

The thinking

A textarea is an Input that grows. It should look and behave like one.

  • The native element: There is no wrapper, so it behaves exactly like a normal textarea in forms and with the browser's spell check.
  • One look for text fields: It shares its styles with Input, so a field of either kind has the same border, colors and states.
  • Inside a Field: To connect a label, a hint and an error to it, render it through FieldControl. The Field page shows how.

Interface design

  • Size: At least 96px tall, with 12px of side padding and 8px above and below, 16px text, and the md radius, 6px.
  • Outline: A 1px border in the secondary text color, 4.2:1 against the page, the same as Input.
  • Resizing: It can be dragged taller or shorter from the bottom corner. It can't be dragged wider, so it can't break the layout.
  • States: Invalid turns the border to the danger color. Disabled is a surface fill with a hairline border and quiet text.

Interaction

  • Hover: The border brightens, but only on a field that is valid and enabled.
  • Focus: A 2px outline, 2px away from the field.
  • Invalid: Keeps its danger border on hover and focus.
  • Disabled: It can't be focused or edited, and shows a not-allowed cursor.

Empathy

  • Name it: Use a Label with htmlFor, or an aria-label. The bare fields in the demo have one.
  • Say what is wrong: Write the message next to the field and tie it in with aria-describedby, as the invalid example does.
  • A placeholder is not a label: It disappears as soon as someone types, and its color is 4.2:1, just under the 4.5:1 that AA asks for.