# Textarea (/docs/textarea)





`Textarea` is the multi-line counterpart to [`Input`](/docs/input). It renders a native `<textarea>` inside the same shell the whole form family uses — the bordered box, the focus-within ring, and the `invalid` / `disabled` / `readOnly` states. Like the rest of the family it carries its own `label`, `description`, `error`, and `required` — no external wrapper needed — and passing `error` flips it to the invalid (red) state. Omit them and it still composes cleanly inside a [`Field`](/docs/field).

```tsx
import { Textarea } from '@42-central/ui-react/textarea';

<Textarea placeholder="How did the defense go?" />
```

Native `<textarea>` props (`value`, `onChange`, `name`, `maxLength`, `ref`, …) forward straight through; `className` styles the shell root and `classNames` overrides individual parts. The control grows automatically to fit its content (`field-sizing: content`); `minRows` and `maxRows` both default to `3`, so an untouched `Textarea` reads as a comfortable multi-line box rather than a single skinny line, and `resizable` opts into the native manual drag handle — off by default, and flush against the shell's border when enabled.

## Playground [#playground]

<story.Playground />

## Variants [#variants]

Two shells. `default` is bordered; `unstyled` strips the chrome but keeps the layout.

<Preview align="block" code="&#x22;<Textarea variant=\&#x22;default\&#x22; placeholder=\&#x22;Default\&#x22; />\n<Textarea variant=\&#x22;unstyled\&#x22; placeholder=\&#x22;Unstyled\&#x22; />&#x22;">
  <div className="flex w-full max-w-[24rem] flex-col gap-4">
    <Textarea variant="default" placeholder="Default" />

    <Textarea variant="unstyled" placeholder="Unstyled" />
  </div>
</Preview>

## Sizes [#sizes]

Five steps — `xs` through `xl`. The control font and padding scale to match `Input`, so a `Textarea` and an `Input` at the same `size` line up.

<Preview align="block" code="&#x22;<Textarea size=\&#x22;xs\&#x22; placeholder=\&#x22;xs\&#x22; />\n<Textarea size=\&#x22;sm\&#x22; placeholder=\&#x22;sm\&#x22; />\n<Textarea size=\&#x22;md\&#x22; placeholder=\&#x22;md\&#x22; />\n<Textarea size=\&#x22;lg\&#x22; placeholder=\&#x22;lg\&#x22; />\n<Textarea size=\&#x22;xl\&#x22; placeholder=\&#x22;xl\&#x22; />&#x22;">
  <div className="flex w-full max-w-[24rem] flex-col gap-3">
    <Textarea size="xs" placeholder="xs" />

    <Textarea size="sm" placeholder="sm" />

    <Textarea size="md" placeholder="md" />

    <Textarea size="lg" placeholder="lg" />

    <Textarea size="xl" placeholder="xl" />
  </div>
</Preview>

## Min & max rows [#min--max-rows]

`minRows` and `maxRows` both default to `3`, so an untouched control already reads as a comfortable multi-line box instead of growing from a single skinny line. Raise `minRows` to start taller, and raise `maxRows` to allow more growth before it switches to internal scrolling — content beyond `maxRows` scrolls instead of growing the box further.

`min-height` wins whenever it conflicts with `max-height`, so raising `minRows` past the default `maxRows` (`3`) *without* also raising `maxRows` produces a fixed-height box rather than a growing one — always raise both together when you want a taller control that still grows.

<Preview align="block" code="&#x22;<Textarea placeholder=\&#x22;Default: 3 rows, capped at 3\&#x22; />\n<Textarea minRows={6} maxRows={12} placeholder=\&#x22;Starts at six rows, grows to twelve, then scrolls\&#x22; />&#x22;">
  <div className="flex w-full max-w-[24rem] flex-col gap-4">
    <Textarea placeholder="Default: 3 rows, capped at 3" />

    <Textarea minRows="6" maxRows="12" placeholder="Starts at six rows, grows to twelve, then scrolls" />
  </div>
</Preview>

## Resizing [#resizing]

`resizable` opts into the native manual drag handle on top of the auto-grow behavior — off by default, since the control already grows to fit what's typed. When enabled, the grip renders flush against the shell's border, and is restyled to match the kit in Chrome, Edge, and Safari (`::-webkit-resizer` has no equivalent in Firefox, which keeps its plain native grip — still fully functional, just unstyled).

<Preview align="block" code="&#x22;<Textarea placeholder=\&#x22;Not resizable (default)\&#x22; />\n<Textarea resizable placeholder=\&#x22;Drag the corner to resize\&#x22; />&#x22;">
  <div className="flex w-full max-w-[24rem] flex-col gap-4">
    <Textarea placeholder="Not resizable (default)" />

    <Textarea placeholder="Drag the corner to resize" />
  </div>
</Preview>

## States [#states]

`invalid` swaps the border and focus ring to red (and sets `aria-invalid`). `disabled` dims the shell and blocks input; `readOnly` keeps full contrast and stays focusable but rejects edits.

<Preview align="block" code="&#x22;<Textarea invalid defaultValue=\&#x22;Too short.\&#x22; />\n<Textarea disabled defaultValue=\&#x22;Locked content\&#x22; />\n<Textarea readOnly defaultValue=\&#x22;Read-only content\&#x22; />&#x22;">
  <div className="flex w-full max-w-[24rem] flex-col gap-4">
    <Textarea defaultValue="Too short." />

    <Textarea defaultValue="Locked content" />

    <Textarea defaultValue="Read-only content" />
  </div>
</Preview>

## With label, description & error [#with-label-description--error]

`label`, `description` (helper text), and `error` are built in via [`Field`](/docs/field) — no external wrapper needed. Passing `error` flips the shell to the invalid (red) state; `required` adds the semantic flag and a red asterisk on the label.

<Preview align="block" code="&#x22;<Textarea label=\&#x22;Bio\&#x22; description=\&#x22;A short blurb for your profile.\&#x22; placeholder=\&#x22;Tell us about yourself…\&#x22; />\n\n<Textarea label=\&#x22;Bio\&#x22; required placeholder=\&#x22;Tell us about yourself…\&#x22; />\n\n<Textarea label=\&#x22;Bio\&#x22; error=\&#x22;Bio is required\&#x22; placeholder=\&#x22;Tell us about yourself…\&#x22; />&#x22;">
  <div className="flex w-full max-w-[24rem] flex-col gap-5">
    <Textarea label="Bio" description="A short blurb for your profile." placeholder="Tell us about yourself…" />

    <Textarea label="Bio" placeholder="Tell us about yourself…" />

    <Textarea label="Bio" error="Bio is required" placeholder="Tell us about yourself…" />
  </div>
</Preview>

If you omit these props, `Textarea` still composes cleanly inside a [`Field`](/docs/field), which propagates `disabled` / `invalid` / `readOnly` / `required` to the control automatically — exactly like `Input`.

```tsx
import { Field } from '@42-central/ui-react/field';
import { Textarea } from '@42-central/ui-react/textarea';

<Field label="Peer feedback" description="What went well, what didn't?">
  <Textarea placeholder="How did the defense go?" />
</Field>
```

## API [#api]

<TypeTable
  type="{
  variant: {
    type: '&#x22;default&#x22; | &#x22;unstyled&#x22;',
    default: '&#x22;default&#x22;',
    description: 'Shell treatment: bordered or chrome-less.',
  },
  size: {
    type: '&#x22;xs&#x22; | &#x22;sm&#x22; | &#x22;md&#x22; | &#x22;lg&#x22; | &#x22;xl&#x22;',
    default: '&#x22;md&#x22;',
    description: 'Height step, control font, and slot icon size.',
  },
  minRows: {
    type: 'number',
    default: '3',
    description: 'Minimum visible text rows; the control grows past it as content wraps.',
  },
  maxRows: {
    type: 'number',
    default: '3',
    description: 'Caps auto-grow at this many rows; content beyond it scrolls instead of growing further. `min-height` wins over `max-height` when they conflict, so raise both together for a taller control that still grows.',
  },
  resizable: {
    type: 'boolean',
    default: 'false',
    description: &#x22;Shows the native drag handle for manual resizing, flush with the shell border.&#x22;,
  },
  label: {
    type: 'ReactNode',
    description: 'Field label above the control.',
  },
  description: {
    type: 'ReactNode',
    description: 'Field helper text below the control.',
  },
  error: {
    type: 'ReactNode',
    description: 'Field error; sets the invalid (red) state.',
  },
  required: {
    type: 'boolean',
    default: 'false',
    description: 'Required flag + asterisk on the label.',
  },
  invalid: {
    type: 'boolean',
    default: 'false',
    description: 'Red border + ring; sets aria-invalid on the textarea.',
  },
  disabled: {
    type: 'boolean',
    default: 'false',
    description: 'Dims the shell and disables the textarea element.',
  },
  readOnly: {
    type: 'boolean',
    default: 'false',
    description: 'Focusable and full-contrast, but the value cannot change.',
  },
  classNames: {
    type: '{ root?, input?, field? }',
    description: 'Per-part class overrides (shell root, inner textarea, field wrapper).',
  },
  testId: {
    type: 'string',
    description: 'data-testid on the real <textarea> element, not the shell wrapper. See the Testing guide.',
  },
}"
/>

`Textarea` forwards every other prop to the underlying `<textarea>` (`value`, `defaultValue`, `onChange`, `name`, `maxLength`, `ref`, …). `className` styles the shell root. `testId` lands on that same `<textarea>` — see the [Testing](/docs/testing) guide.

## Accessibility [#accessibility]

* Pass `label` / `description` / `error` to wire the accessible name and `aria-*` relationships via the built-in [`Field`](/docs/field). Omit them and it composes inside an outer `Field` (or your own `<label htmlFor>`) instead.
* `invalid` sets `aria-invalid` on the textarea so assistive tech announces the error state.
* The focus ring is `focus-within`, so it appears whenever the nested textarea is focused.
* RTL: the shell is a plain flex layout, so padding flips automatically under `dir="rtl"`.
