---
title: Textarea
description: A textarea in the same clothes as Input, growing with its content where the browser allows.
source: https://adamjurek.com/components/textarea
---

## Home


A textarea styled like Input, using `field-sizing: content` so it grows with
what's typed wherever the browser supports it, with a `rows` fallback where it
doesn't. Reach for it for multi-line free text - a message, a description -
where the number of lines isn't known ahead of time.

<!-- ::start:showcase demo="textarea" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

Textarea is a native `<textarea>` with two additions: a default of 4 rows, and
the same `field-control` class Input carries, so the two feel like siblings
rather than related-but-different controls. `field-sizing: content` does the
actual growing where the browser supports it; `rows` is what shows before
anything is typed, and the honest fallback everywhere else.

> [!NOTE] Install commands are not written here
> Anything in `packages/ui/registry.ts` gets its TanStack and shadcn commands
> attached automatically, so there is nothing to keep in sync.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/textarea.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/textarea.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | content · Forms |
| Files | `textarea.tsx` |
| Dependencies | None |
| Tags | form, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { Textarea } from "@sushindustries/ui";

export function Example() {
	return <Textarea placeholder="Say what happened, in order." />;
}
```

## What you should see

A field styled exactly like `Input`, four rows tall by default. Where the
browser supports `field-sizing: content`, typing past the bottom grows the
box instead of scrolling it - the field gets taller, the page does not
scroll inside it. There is a drag handle in the corner either way, because
`resize: vertical` stays on regardless.

## If nothing happens

If the field never grows past its starting height no matter how much is
typed, the browser does not support `field-sizing: content` yet - Safari is
the notable holdout. That is a graceful fallback, not a bug: the field
keeps its fixed height and its resize handle, the same as any plain
`<textarea>`.


## Guides


## Composing it

`Textarea` already carries `field-control field-textarea` - any `className`
passed in is appended, not swapped in, so a caller only ever adds to the
control's look rather than needing to reconstruct it. Pair it with `Field`
for a label, a hint or an error state; `Textarea` on its own is the control
alone, the same as `Input`.

## When not to use it

For a single line of free text, `Input` is the right size - `Textarea`'s
growing behaviour and taller minimum height cost vertical space a one-line
field does not need. Reach for this one when the content is genuinely
multi-line: a message, a description, anything where the number of lines
isn't known ahead of time.


## API


<!-- generated:api -->

## Props

Accepts every prop of `TextareaHTMLAttributes<HTMLTextAreaElement>`.

<!-- /generated:api -->

## Notes

`rows` defaults to 4 when left unset, but it only sets the *starting*
height. Where `field-sizing: content` is supported, the visible height
tracks what is typed and the stylesheet's own `min-height: 4.5em` does the
same job `rows` would otherwise do - so passing a larger `rows` rarely
changes what a modern browser shows. It still matters as the fallback
height in browsers without `field-sizing` support.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="textarea" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { Field, Textarea } from "@sushindustries/ui";
import { useState } from "react";

export function FeedbackForm() {
	const [message, setMessage] = useState("");

	return (
		<form className="flex flex-col gap-4">
			<Field label="What happened" hint="As much detail as you can give">
				<Textarea
					value={message}
					onChange={(event) => setMessage(event.target.value)}
					placeholder="Say what happened, in order."
				/>
			</Field>
			<button type="submit" className="btn">
				Send
			</button>
		</form>
	);
}
```

## What this example is not

There is no character counter or max-length handling here - `maxLength` is
a native attribute `Textarea` passes through untouched, but showing the
count as someone types is markup the caller adds, not something this
component renders for you.
