# Shadcn Textarea

> Free shadcn/ui textarea component for React — auto-growing (field-sizing), character counter, resize control and validation examples. Copy or install in one command.

Source: https://designrevision.com/components/textarea

The **Shadcn Textarea** is the multi-line counterpart to the [Input](/components/input) — the canonical <a href="https://ui.shadcn.com/docs/components/textarea" rel="nofollow">shadcn/ui</a> textarea, a native `<textarea>` themed entirely with CSS variables. It ships one quietly powerful default: the box **auto-grows with its content** via the CSS `field-sizing-content` property, so it behaves like a polished chat or comment input out of the box, with no autosize library. Pair it with a [Label](/components/label) and a [Button](/components/button); the examples below cover the patterns you reach for most.

## When to use

Use a textarea for **longer, free-form, multi-line text** — a message, a comment, a bio, a description. For a single line (email, name, search) use an [Input](/components/input); for a fixed set of choices use a select or radio group. Match the field size to the expected answer: a one-sentence summary and a full support message should not look the same.

## Autosize and sizing

The primitive includes `field-sizing-content`, so the height tracks the text as the user types — no `useEffect`, no `react-textarea-autosize`. Set `min-h-*` for the resting height and `max-h-*` to cap the growth (past the cap it scrolls). This is the modern, dependency-free replacement for JavaScript autosize, and it degrades gracefully to a normal `rows`-sized box where `field-sizing` is unsupported.

## Resize control

By default the browser shows a drag handle. Add `resize-y` to allow vertical resizing only, or `resize-none` to remove the handle entirely — handy when the height is driven by autosize and you do not want users fighting it. The Resize control example shows both side by side.

## Validation and states

Error state is driven by the standard **`aria-invalid`** attribute — set it and the textarea switches on the destructive border and ring automatically, no custom prop. This keeps it compatible with native validation and form libraries like React Hook Form. Always pair an invalid field with a visible message linked by `aria-describedby`. Disabled and read-only use the native attributes, so they behave and announce correctly.

## Accessibility

Every textarea is keyboard-navigable with a visible 3px focus ring. Give each one a bound `Label` (or an `aria-label` when the design has no visible label). When you cap length with `maxLength`, surface the limit visibly — a counter — so the constraint is not a surprise on submit.

## Theming

The textarea reads your design tokens (`--input`, `--ring`, `--destructive`, `--muted-foreground`), so it follows your theme — including dark mode — with zero overrides. Any palette produced by a shadcn-compatible theme generator applies automatically.

## Installation

### textarea

**Install:**

```bash
npx shadcn@latest add @designrevision/textarea
```

**Dependencies:** utils

**Usage & accessibility:** Extends React.ComponentProps<"textarea"> — all native <textarea> attributes are supported. Errors switch on via aria-invalid; field-sizing-content auto-grows the box, so set rows, min-h, or max-h to bound it.

```tsx
// components/ui/textarea.tsx
import * as React from "react"

import { cn } from "@/lib/utils"

function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
  return (
    <textarea
      data-slot="textarea"
      className={cn(
        "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
        className
      )}
      {...props}
    />
  )
}

export { Textarea }
```

## Examples

### With label

The canonical multi-line field — a `Label` bound by `htmlFor`, with muted helper text below.

**Install:**

```bash
npx shadcn@latest add @designrevision/textarea-label-01
```

**Dependencies:** textarea, label

```tsx
// components/ui/textarea-label-01.tsx
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"

export default function TextareaLabel01() {
  return (
    <div className="grid w-full max-w-sm gap-2">
      <Label htmlFor="message">Your message</Label>
      <Textarea id="message" placeholder="Type your message here…" />
      <p className="text-sm text-muted-foreground">
        We typically reply within a business day.
      </p>
    </div>
  )
}
```

---

### Autosize

Grows with its content up to a `max-h` and then scrolls — powered by the CSS `field-sizing-content` baked into the primitive, with no autosize library.

**Install:**

```bash
npx shadcn@latest add @designrevision/textarea-autosize-01
```

**Dependencies:** textarea, label

```tsx
// components/ui/textarea-autosize-01.tsx
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"

export default function TextareaAutosize01() {
  return (
    <div className="grid w-full max-w-sm gap-2">
      <Label htmlFor="bio">Bio</Label>
      <Textarea
        id="bio"
        placeholder="Start typing — the field grows with your text…"
        defaultValue="The height tracks the content via field-sizing-content. It grows as you type, stops at the max height, then scrolls."
        className="max-h-40 resize-none"
      />
      <p className="text-sm text-muted-foreground">
        Grows with content up to a max height — no JavaScript.
      </p>
    </div>
  )
}
```

---

### Character counter

A live `current/max` count in `tabular-nums`, backed by the textarea’s `maxLength`.

**Install:**

```bash
npx shadcn@latest add @designrevision/textarea-counter-01
```

**Dependencies:** textarea, label

```tsx
// components/ui/textarea-counter-01.tsx
"use client"

import * as React from "react"

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

export default function TextareaCounter01() {
  const maxLength = 180
  const [value, setValue] = React.useState("")

  return (
    <div className="grid w-full max-w-sm gap-2">
      <Label htmlFor="about">About you</Label>
      <Textarea
        id="about"
        value={value}
        maxLength={maxLength}
        onChange={(e) => setValue(e.target.value)}
        placeholder="A short bio…"
      />
      <p className="text-right text-sm tabular-nums text-muted-foreground">
        {value.length}/{maxLength}
      </p>
    </div>
  )
}
```

---

### With button

A comment field paired with a submit `Button` — the shape behind comment boxes and reply forms.

**Install:**

```bash
npx shadcn@latest add @designrevision/textarea-button-01
```

**Dependencies:** textarea, label, button

```tsx
// components/ui/textarea-button-01.tsx
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"

export default function TextareaButton01() {
  return (
    <form className="grid w-full max-w-sm gap-2">
      <Label htmlFor="comment">Add a comment</Label>
      <Textarea id="comment" placeholder="Share your thoughts…" />
      <div className="flex justify-end">
        <Button type="submit">Comment</Button>
      </div>
    </form>
  )
}
```

---

### States

Invalid (`aria-invalid` switches on the destructive ring and border, paired with `aria-describedby`), disabled, and read-only fields.

**Install:**

```bash
npx shadcn@latest add @designrevision/textarea-states-01
```

**Dependencies:** textarea, label

```tsx
// components/ui/textarea-states-01.tsx
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"

export default function TextareaStates01() {
  return (
    <div className="grid w-full max-w-sm gap-6">
      <div className="grid gap-2">
        <Label htmlFor="message-invalid">Message</Label>
        <Textarea
          id="message-invalid"
          defaultValue="Too short"
          aria-invalid="true"
          aria-describedby="message-invalid-error"
        />
        <p id="message-invalid-error" className="text-sm text-destructive">
          Message must be at least 20 characters.
        </p>
      </div>
      <div className="grid gap-2">
        <Label htmlFor="message-disabled">Disabled</Label>
        <Textarea id="message-disabled" placeholder="Unavailable…" disabled />
      </div>
      <div className="grid gap-2">
        <Label htmlFor="message-readonly">Read only</Label>
        <Textarea
          id="message-readonly"
          readOnly
          defaultValue="This content can be selected and copied, but not edited."
        />
      </div>
    </div>
  )
}
```

---

### Resize control

Constrain the drag handle with `resize-y` for vertical-only resizing, or `resize-none` to remove it entirely.

**Install:**

```bash
npx shadcn@latest add @designrevision/textarea-resize-01
```

**Dependencies:** textarea, label

```tsx
// components/ui/textarea-resize-01.tsx
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"

export default function TextareaResize01() {
  return (
    <div className="grid w-full max-w-sm gap-6">
      <div className="grid gap-2">
        <Label htmlFor="ta-resize-y">Vertical resize handle</Label>
        <Textarea
          id="ta-resize-y"
          className="resize-y"
          placeholder="Drag the bottom-right corner to resize…"
        />
      </div>
      <div className="grid gap-2">
        <Label htmlFor="ta-resize-none">No resize handle</Label>
        <Textarea
          id="ta-resize-none"
          className="resize-none"
          placeholder="Manual resizing disabled…"
        />
      </div>
    </div>
  )
}
```
