# Textarea

An accessible React textarea for Next.js that grows smoothly with its content, with min and max rows, styled like Input with Tailwind CSS.

Source: https://ui.fewtone.com/components/textarea

## Installation

```bash
npx shadcn@latest add https://ui.fewtone.com/r/textarea.json
```

## Usage

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

export function MessageField() {
  return (
    <>
      <label htmlFor="message">Message</label>
      <Textarea id="message" minRows={2} maxRows={6} />
    </>
  )
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `minRows` | `number` | `3` | The lines it shows when empty. It never gets shorter. |
| `maxRows` | `number` | – | The lines it grows to before it scrolls. Unlimited when left out. |
| `aria-invalid` | `boolean` | – | Marks the value as wrong: the border and focus ring turn destructive. |
| `maxLength` | `number` | – | The most characters it accepts. |
| `disabled` | `boolean` | `false` | Prevents editing. |

**When to use it:** Use Textarea for multi-line text such as messages, comments, descriptions and bios, always with a visible label. Set minRows and maxRows to bound how far it grows. Use Input for a single line.
**npm packages (installed by the CLI):** motion@^14.0.0.

## Examples

### Default

Type a few lines: the field glides taller a line at a time, up to 10 lines, then scrolls. Delete them and it glides back to 4. Drag its bottom-right corner to set its height.

```tsx
import { Textarea } from "@/components/ui/textarea";
import { useId } from "react";

export default function TextareaDefault() {
  const id = useId();
  return (
    <div className="grid w-full max-w-md gap-2">
      <label htmlFor={id} className="text-sm font-medium">
        Message
      </label>
      <Textarea id={id} placeholder="Write a few lines…" minRows={4} maxRows={10} />
    </div>
  );
}
```

### Character counter

maxLength stops the text at 160 characters. Each digit of the count swaps through the blur on its own, rising as you type and falling as you delete.

```tsx
"use client";

import { Textarea } from "@/components/ui/textarea";
import { duration, ease, spring, swap } from "@/lib/motion";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useId, useState } from "react";

const LIMIT = 160;

/*
 * A character count under the field. Each digit swaps through the blur on its own (Copy Button's swap),
 * rising as the count goes up and falling as it goes down, so typing rolls only the digits that change.
 * A digit that appears or goes (9 → 10) comes and goes the same way. Screen readers get the count as text,
 * linked to the field with aria-describedby.
 */

/** The swap, turned to the count's direction: 1 rises for a higher count, -1 falls for a lower one */
const roll = {
  enter: (direction: number) => ({ ...swap.initial, y: swap.initial.y * direction }),
  shown: swap.animate,
  leave: (direction: number) => ({ ...swap.exit, y: swap.exit.y * direction }),
};
/** With reduced motion, opacity only */
const fade = { enter: { opacity: 0 }, shown: { opacity: 1 }, leave: { opacity: 0 } };

function Count({ value }: { value: number }) {
  const reduce = useReducedMotion() ?? false;
  // Which way the count last moved, updated while rendering (React's pattern for following a prop)
  const [last, setLast] = useState(value);
  const [direction, setDirection] = useState(1);
  if (value !== last) {
    setLast(value);
    setDirection(value > last ? 1 : -1);
  }
  const digits = String(value).split("");
  const variants = reduce ? fade : roll;
  const transition = reduce
    ? { duration: duration.fast }
    : { duration: duration.base, ease: ease.out, y: spring.snappy };
  const swapping = {
    custom: direction,
    variants,
    initial: "enter",
    animate: "shown",
    exit: "leave",
    transition,
  };

  return (
    <span aria-hidden className="inline-flex">
      <AnimatePresence initial={false} custom={direction}>
        {digits.map((digit, index) => {
          // Keyed by place (ones, tens, hundreds), so each digit keeps its slot as the number grows
          const place = digits.length - index;
          return (
            <motion.span key={place} {...swapping} className="inline-grid">
              <AnimatePresence initial={false} custom={direction}>
                {/* Old and new share one cell, so the swap doesn't shift the text */}
                <motion.span key={digit} {...swapping} className="col-start-1 row-start-1">
                  {digit}
                </motion.span>
              </AnimatePresence>
            </motion.span>
          );
        })}
      </AnimatePresence>
    </span>
  );
}

export default function TextareaCounter() {
  const id = useId();
  const [bio, setBio] = useState("Designer and engineer. I build calm, accessible interfaces.");

  return (
    <div className="grid w-full max-w-sm gap-2">
      <label htmlFor={id} className="text-sm font-medium">
        Bio
      </label>
      <Textarea
        id={id}
        value={bio}
        onChange={(event) => setBio(event.target.value)}
        maxLength={LIMIT}
        minRows={2}
        maxRows={5}
        aria-describedby={`${id}-hint ${id}-count`}
      />
      <div className="flex items-start justify-between gap-4 text-sm text-muted-foreground">
        <p id={`${id}-hint`}>Shown on your public profile.</p>
        <p id={`${id}-count`} className="shrink-0 tabular-nums">
          <Count value={bio.length} />
          <span aria-hidden>/{LIMIT}</span>
          <span className="sr-only">
            {bio.length} of {LIMIT} characters
          </span>
        </p>
      </div>
    </div>
  );
}
```

### Disabled

```tsx
import { Textarea } from "@/components/ui/textarea";
import { useId } from "react";

export default function TextareaDisabled() {
  const id = useId();
  return (
    <div className="grid w-full max-w-sm gap-2">
      <label htmlFor={id} className="text-sm font-medium">
        Release notes
      </label>
      <Textarea id={id} minRows={2} defaultValue="Locked while the release is being published." disabled />
    </div>
  );
}
```

## Related

- [Input](https://ui.fewtone.com/components/input.md): An accessible React text input with three sizes and clear invalid, disabled and file states, built on a native input.
