mwui
GitHub

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

PropDescription
minRowsStarting height in rows. Defaults to 2.
maxRowsStop growing after this many rows and scroll. Omit for no limit.
scrollAreaScroll the overflow inside a ScrollArea instead of the native textarea scrollbar. Defaults to true. Needs maxRows.
onHeightChangeCalled 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-height and 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.
  • onHeightChange reports the visible height, so it stops at maxRows rather than reporting the full content height of a scrolled field.