# Textarea

Enter and edit multiline text.

Project description

[Complete TSX file](/examples/textarea/default.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";
import { Textarea } from "@/siteplane/components/textarea";

export default function Example() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Description</FieldLabel>
      <Textarea placeholder="What is this project about?" />
      <FieldDescription>Shown on the project overview.</FieldDescription>
    </Field>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/textarea
```

With npm: `npx shadcn@4.18.0 add @siteplane/textarea`. [Registry JSON](/registry/textarea.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/input
```

Field is installed with Input.

## Usage

```tsx
import { Textarea } from "@/siteplane/components/textarea";

<Textarea aria-label="Description" />;
```

## Variants

Choose the control surface without changing its behavior. `elevated` lifts the field with the hairline and soft shadow of an elevated Card, so it reads on a white surface too.

Textarea surface variants

[Complete TSX file](/examples/textarea/variants.tsx)

```tsx
"use client";

import { Field, FieldLabel } from "@/siteplane/components/field";
import { Textarea } from "@/siteplane/components/textarea";

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      {(["default", "elevated", "inset"] as const).map((variant) => (
        <Field key={variant}>
          <FieldLabel>Description, {variant}</FieldLabel>
          <Textarea
            variant={variant}
            placeholder="What is this project about?"
          />
        </Field>
      ))}
    </div>
  );
}
```

## Height by lines

`minLines` sets the least height of the textarea and `maxLines` the most, in text lines plus the padding of the size. Both take fractions, such as 2.5 lines for a description. In between, the textarea grows with its content; past `maxLines` its text scrolls.

Reply between two and six lines

[Complete TSX file](/examples/textarea/lines.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";
import { Textarea } from "@/siteplane/components/textarea";

export default function Example() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Reply</FieldLabel>
      <Textarea maxLines={6} minLines={2} placeholder="Write a reply…" />
      <FieldDescription>
        The customer gets your reply by email.
      </FieldDescription>
    </Field>
  );
}
```

## Sizes

Omit `size` to inherit the provider's control size. Each size sets its own minimum height. The text is 14px, or 16px at `xl`; `textSize` sets it at any size. Touch screens always use 16px text.

Textarea sizes

[Complete TSX file](/examples/textarea/sizes.tsx)

```tsx
"use client";

import { Field, FieldLabel } from "@/siteplane/components/field";
import { Textarea } from "@/siteplane/components/textarea";

const sizes = [
  { size: "sm", label: "small" },
  { size: "md", label: "medium" },
  { size: "lg", label: "large" },
  { size: "xl", label: "extra large" },
] as const;

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      {sizes.map(({ size, label }) => (
        <Field key={size}>
          <FieldLabel>Description, {label}</FieldLabel>
          <Textarea
            size={size}
            defaultValue="Redesign the marketing site and move it to the new CMS."
          />
        </Field>
      ))}
    </div>
  );
}
```

## Disabled, read-only and invalid

`disabled` blocks editing; `readOnly` keeps the value focusable and submitted. Set `invalid` on Field and explain the problem with FieldError.

Disabled, read-only and invalid textareas

[Complete TSX file](/examples/textarea/states.tsx)

```tsx
"use client";

import { Field, FieldError, FieldLabel } from "@/siteplane/components/field";
import { Textarea } from "@/siteplane/components/textarea";

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      <Field disabled>
        <FieldLabel>Internal notes</FieldLabel>
        <Textarea defaultValue="Waiting for the final copy from the content team." />
      </Field>
      <Field>
        <FieldLabel>Release notes</FieldLabel>
        <Textarea
          readOnly
          defaultValue="Version 2.4 adds shared views and faster exports."
        />
      </Field>
      <Field invalid>
        <FieldLabel>Summary</FieldLabel>
        <Textarea defaultValue="Fixes" />
        <FieldError match>Write at least 20 characters.</FieldError>
      </Field>
    </div>
  );
}
```

## Character limit

Control the value with `value` and `onChange` to show the remaining characters. `maxLength` enforces the limit.

Bio with character limit

[Complete TSX file](/examples/textarea/controlled.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";
import { Textarea } from "@/siteplane/components/textarea";
import { useState } from "react";

const maxLength = 280;

export default function Example() {
  const [value, setValue] = useState("");

  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Bio</FieldLabel>
      <Textarea
        maxLength={maxLength}
        placeholder="Tell your team a little about yourself."
        value={value}
        onChange={(event) => setValue(event.target.value)}
      />
      <FieldDescription>
        <span className="tabular-nums">{maxLength - value.length}</span>{" "}
        characters left
      </FieldDescription>
    </Field>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| size | sm \| md \| lg \| xl | provider / lg | Visual control size. |
| textSize | sm \| base | unset | Text size at any control size: sm is 14px, base is 16px. Unset uses 14px, or 16px at xl; touch screens use 16px. |
| minLines | number | unset | Least height in text lines, fractions allowed, plus the padding of the size. Replaces the minimum height of the size; the textarea still grows with its content. |
| maxLines | number | unset | Most height in text lines, fractions allowed, plus the padding of the size. The textarea grows up to it, then scrolls; unset, it grows without a limit. minLines wins where the two disagree. |
| variant | default \| elevated \| inset | default | Surface treatment. |
| unstyled | boolean | false | Removes the outer surface recipe; preserves control anatomy. |
| className | string | unset | Styles the outer span, not the native control. |

`TextareaProps` and `TextareaProfile` export the types above. `FieldPrimitive` is also exported. Native textarea props, including required, name, value, defaultValue, readOnly, onChange and ref, reach the actual textarea through Field Control. There is no onValueChange prop. Because the textarea sizes to its content, the native `rows` does not set its height: use `minLines` and `maxLines`.

Other native attributes follow [HTML textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/textarea).

[Read the installed source](/source/textarea).

## Usage notes

Always provide a visible label or an accessible name. Placeholder text is not a label. Use Field for related help and errors. Where the browser supports `field-sizing: content`, the textarea grows with its content and shows no resize grip; in other browsers it keeps its height and a vertical resize grip. There is no autoGrow prop. The control does not persist data or implement request loading. A plain native form and the kit Form have different validation presentation; see [Forms](/docs/guides/forms/).
