# Textarea

> An auto-growing React textarea with a character counter, an optional resize handle, a toolbar slot and mod+enter to submit.

- Type: React component (Inputs)
- Access: Free (the source is open to everyone)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add textarea`
- Docs: https://wingo-ui.com/components/textarea

## Interaction

Type and the box grows line by line from minRows up to maxRows, then scrolls inside; growth is instant, so typing never lags, and the caret stays in view because the box grows, not a scroller. Enter makes a new line and Tab moves on, like a native textarea. Press ⌘ Enter (Ctrl Enter elsewhere) to submit: the playground's Post checks for at least 40 characters, slides the error in under the box, and sends once it is long enough; submitShortcut="enter" makes it chat style, with Shift Enter for a new line. Use an example (Example on a phone) fills a long description so you can watch it grow to maxRows and scroll; Clear clears it. With a limit and showCount the counter turns amber at 90% and red at the limit. With resizable, a grip in the corner lets a mouse drag the box taller and that height sticks as the new floor. A click anywhere in the box, even in the toolbar's empty space, lands in the text. On a phone the text is 16px so iOS never zooms, the return key makes new lines, toolbar buttons keep 44px targets and the grip hides.

## Installation

```bash
npx wingo-ui@latest add textarea
```

The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add https://wingo-ui.com/r/textarea.json`

Files:

- `components/ui/textarea.tsx`

Wingo UI dependencies (installed automatically):

- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { Paperclip, SendHorizontal } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"

export function ListingDescription() {
  return (
    <form action="/listings" method="post">
      <Textarea
        name="description"
        label="Listing description"
        description="At least 40 characters."
        placeholder="Describe the item: condition, size, why you are selling."
        required
        minLength={40}
        showCount
        maxLength={9000}
        fieldProps={{ counterLabel: (left) => `${left} characters left` }}
      />
    </form>
  )
}

export function Composer({ onSend }: { onSend: (text: string) => void }) {
  const [text, setText] = useState("")
  const send = () => {
    if (!text.trim()) return
    onSend(text.trim())
    setText("")
  }
  return (
    <Textarea
      aria-label="Message"
      placeholder="Write a message…"
      value={text}
      onValueChange={setText}
      minRows={1}
      maxRows={6}
      onSubmitShortcut={send}
      footer={
        <>
          <Button variant="ghost" size="icon-sm" radius="full" aria-label="Attach">
            <Paperclip />
          </Button>
          <Button size="sm" radius="full" className="ms-auto" rightIcon={<SendHorizontal />} onClick={send}>
            Send
          </Button>
        </>
      }
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label?` | `ReactNode` | `""` | The label, linked with htmlFor. Leave it empty for a bare box, e.g. inside a <Field> that has its own label. |
| `description?` | `ReactNode` | `""` | Helper text under the box, linked with aria-describedby. The error takes its place while it shows. |
| `error?` | `ReactNode` | `""` | Replaces the description with an alert icon and the text, slides in and marks the box invalid (red ring, aria-invalid). |
| `required?` | `boolean` | `false` | The required mark after the label and required on the textarea. |
| `invalid?` | `boolean` | `false` | The invalid look and aria-invalid without a message, for forms that show errors elsewhere. |
| `disabled?` | `boolean` | `false` | Dims the box and its label and blocks typing. Not submitted with the form, like any disabled field. |
| `readOnly?` | `boolean` | `false` | The text stays visible, selectable and scrollable but cannot change; no hover tint and no resize grip. Good for terms or a sent message. |
| `placeholder?` | `string` | `""` | Shown while the box is empty, in the muted text color. |
| `defaultValue?` | `string` | `""` | The starting text while uncontrolled. Where the browser has CSS field-sizing the box fits it from the first paint, before any JavaScript runs. |
| `value?` | `string` |  | The text, to control it from outside; pair it with onValueChange. A controlled value past maxLength shows as given (the counter turns red) and is never cut silently. |
| `onValueChange?` | `(value: string) => void` |  | Called with the new text on every change, controlled or not. |
| `onChange?` | `(event: ChangeEvent<HTMLTextAreaElement>) => void` |  | The native change event, untouched, so {...register("description")} from react-hook-form works as on a plain textarea. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Text 13 / 14 / 15px (16px on phones, so iOS never zooms) with matching padding. One row is as tall as an input of the same size: 32 / 40 / 48px, 44px for md on touch; sm grows to 36px on phones, where its text is 16px. |
| `variant?` | `"soft" \| "outline" \| "underline" \| "ghost"` | `"soft"` | soft is a filled well that works on every surface, outline a bordered box, underline a bottom line that grows a bar from the center on focus, ghost is invisible until hovered (inline edits, toolbars). |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner shape, scaled per size like the button's (md is 8 / 10 / 12px). full is half a one-row height (16 / 20 / 24px; 18px for sm on phones, 22px for md on touch), so a one-row composer is a pill and a taller box never turns into an oval. Ignored by underline. |
| `minRows?` | `number` | `3` | The height when empty, in rows of text. 1 makes a composer that starts as tall as an input. |
| `maxRows?` | `number` | `8` | Grows up to this many rows, then scrolls inside. 0 means no limit; a value below minRows counts as minRows. |
| `autosize?` | `boolean` | `true` | Grow with the text. Uses CSS field-sizing where the browser has it and measures once per change elsewhere. false keeps the box at minRows and scrolls. |
| `resizable?` | `boolean` | `false` | A vertical resize grip for mouse and trackpad (hidden on touch). With autosize the dragged height becomes the new floor and the box still grows past it when the text needs more. |
| `showCount?` | `boolean` | `false` | The "count / max" counter at the end of the message row, amber from 90% and red at the limit. Needs maxLength. |
| `maxLength?` | `number` | `0` | The native character limit; 0 means none. Screen readers hear how many characters are left once 10% or less remains. |
| `submitShortcut?` | `string` | `"mod+enter"` | The combo that submits, in mod+k notation: ⌘ Enter on Apple devices, Ctrl Enter elsewhere. It calls onSubmitShortcut, else form.requestSubmit(). "enter" makes it chat style (Shift Enter for a new line; touch keyboards keep making new lines), "" turns it off. Announced with aria-keyshortcuts; shown nowhere by default (the demo shows a Kbd hint). |
| `onSubmitShortcut?` | `(value: string, event: KeyboardEvent<HTMLTextAreaElement>) => void` |  | Runs on the shortcut with the current text instead of submitting the form. The chat example sends the message with it. |
| `header?` | `ReactNode` |  | A row inside the box above the text: attachment chips, a quoted reply. A click on its empty space lands in the text. |
| `footer?` | `ReactNode` |  | A toolbar inside the box under the text: attach, emoji, send. Buttons keep their own 44px hit areas on touch; a click on the empty space lands in the text. |
| `id?` | `string` |  | The textarea's id. Defaults to useId(); inside a <Field> the Field's id wins. ref and every other native prop (name, autoComplete, spellCheck, autoFocus, onBlur, aria-*, data-*) land on the <textarea> too; inputMode defaults to text and enterKeyHint to enter. |
| `fieldProps?` | `Partial<FieldProps>` |  | Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel, counterLabel (what screen readers hear near the limit). |
| `classNames?` | `Partial<Record<"field" \| "label" \| "description" \| "error" \| "counter" \| "box" \| "textarea" \| "header" \| "footer", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Classes for the outermost element: the Field root when there is a label, description, error or counter, else the box. |

## Examples

- **Counter and minimum length**: showCount with a limit, a 40-character minimum checked on blur, and the counter turning amber near 120.
- **Chat composer**: minRows 1 to maxRows 6 with attachment chips in header and a toolbar in footer. onSubmitShortcut sends; switch to Enter for chat style.
- **Autosize, fixed and resizable**: Grows with the text, stays fixed and scrolls, or takes a dragged height as its new floor.
- **Variants and sizes**: soft, outline, underline and ghost in sm, md and lg. One row is as tall as an input of the same size.
- **Read-only terms**: readOnly at maxRows 5: selectable, scrollable, not editable.
- **States**: Required with a hint, an error, invalid without a message, and disabled.
- **In a horizontal form**: Inside a FieldGroup the label moves beside the box once the form is wide enough and stacks on a phone. Cancel resets the form, ⌘ Enter saves it.

## See also

- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Kbd](https://wingo-ui.com/components/kbd) ([Markdown](https://wingo-ui.com/components/kbd.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))

---

Canonical: https://wingo-ui.com/components/textarea
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
