Autosize Textarea
A textarea that grows with its content between row bounds.
92 characters · 2 lines
Installation
Usage
A textarea that starts at a comfortable size, grows as the message gets longer, and stops growing before it eats the page. The interaction every composer, comment box, and prompt field needs.
import { AutosizeTextarea } from "@/components/ui/autosize-textarea";
export function Composer() {
const [message, setMessage] = React.useState("");
return (
<AutosizeTextarea
value={message}
onChange={(event) => setMessage(event.target.value)}
minRows={2}
maxRows={10}
placeholder="Write a message…"
/>
);
}Past maxRows the field stops growing and scrolls internally. Without a maximum it grows without
limit, which is fine inside a page but wrong inside a fixed composer.
It is a real <textarea> — every prop, ref, and form behavior passes straight through — so
required, maxLength, name, and controlled or uncontrolled use all work as usual.
Scrolling past maxRows
A <textarea> is its own scroll container, so capping its height normally hands you the native
scrollbar — the one piece of a styled form that still looks like the operating system. Instead the
field is allowed to grow to its full content height and a ScrollArea
clips it to the row cap, so the overflow gets the same overlay scrollbar as the rest of the app.
The frame — border, background, focus ring, invalid state — moves to that container, and className
follows it, so styling the component still means styling the box you can see. Typing still keeps the
caret in view: the browser scrolls the nearest scrollable ancestor, which is now the viewport.
<AutosizeTextarea maxRows={6} scrollArea={false} />Pass scrollArea={false} for the native scrollbar and a plain <textarea> with no wrapper — worth
it if you are dropping the field into something that already measures or scrolls its children. The
prop does nothing without maxRows, since an unbounded field never scrolls.
Why not field-sizing: content
The CSS property does this natively in one line, and it is the right answer once support is universal. Today it is missing in Safari and Firefox, where the field would silently stop resizing. This measures instead, so it behaves the same everywhere.
Resizing when the width changes
Height depends on width: a sidebar collapsing, a modal opening, or a window resize can rewrap the text and change how many lines it takes. The component observes its own width with useResizeObserver and re-measures — which a value-only effect would miss, leaving a field that is suddenly too short.
Props
| Prop | Description |
|---|---|
minRows | Starting height in rows. Defaults to 2. |
maxRows | Stop growing after this many rows and scroll. Omit for no limit. |
scrollArea | Scroll the overflow inside a ScrollArea instead of the native textarea scrollbar. Defaults to true. Needs maxRows. |
onHeightChange | Called with the new pixel height — useful for keeping a scroll container pinned to the bottom. |
Everything else is forwarded to the underlying textarea.
Behavior
- Measurement runs in a layout effect, before paint, so the height never flickers.
line-heightand padding are read from the computed style, so the row math follows your CSS instead of a hardcoded number.- Manual resizing is disabled, since the drag handle would fight the automatic height.
onHeightChangereports the visible height, so it stops atmaxRowsrather than reporting the full content height of a scrolled field.