Inputs
Autosize Textarea
A textarea that grows with its content, from a minimum row count up to a capped maximum before it scrolls internally.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/autosize-textarea.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "AutosizeTextarea" component (no external deps).
Contract
- Export a forwardRef component whose ref points at the inner <textarea>;
props extend TextareaHTMLAttributes<HTMLTextAreaElement>.
- minRows?: number (default 2) — visible rows before it starts growing.
- maxRows?: number (default 8) — rows it grows to before switching to
internal scrolling instead of growing further.
- Fully supports both controlled (value + onChange) and uncontrolled
(defaultValue) usage, passed straight through to the inner textarea.
Behavior
- Two implementations, feature-detected once via
CSS.supports("field-sizing", "content") (guard for SSR where `CSS` is
undefined):
1. Supported (2024+ Chromium/Firefox): set the CSS `field-sizing: content`
property on the element plus min-height/max-height in pixels — the
browser grows the box natively, zero JS on every keystroke.
2. Unsupported: on the native "input" event, set height to "auto" then to
scrollHeight clamped between the same min/max pixel bounds, and toggle
overflow-y between "hidden" and "auto" depending on whether content
exceeds the cap. Controlled value changes don't fire a native input
event, so also re-run this measurement in a useLayoutEffect keyed on
the `value` prop (DOM reads/writes only, no setState there).
- minRows/maxRows convert to pixels using the element's own computed
line-height (plus vertical padding/border) read via getComputedStyle,
measured once on first resize and cached in a ref — this makes the
bounds correct for whatever font size/line-height className brings in,
without hardcoding a px number.
- resize is disabled (resize-none) — growth is automatic, never a manual
drag handle.
Rendering & styling
- Mirrors the shadcn Textarea look: rounded-md border border-input
bg-transparent px-3 py-2, focus-visible:border-ring +
focus-visible:ring-3 focus-visible:ring-ring/50, disabled and
aria-invalid states via their respective Tailwind variants. Semantic
tokens only, no hardcoded hex/oklch. Merge the consumer's className and
style via cn() / spread so callers can still override.
- rows attribute defaults to minRows so the box already has a sane height
before hydration/JS runs (progressive enhancement, not a loading flash).
Customization levers
- minRows / maxRows: tune the floor and the scroll ceiling per surface
(a compact comment box vs. a tall message composer).
- Typography: font-size/line-height are read live from computed styles,
so swapping className's text size classes just works without touching
the sizing logic.
- react-hook-form: spread {...field} onto it like any other controlled
textarea — value/onChange are unmodified, no adapter needed.Concepts
- Progressive enhancement via
field-sizing: content— a 2024+ CSS property that lets the browser own the growth, so supporting browsers do this with zero per-keystroke JavaScript. scrollHeightfallback — older browsers get a classic measure-and-set: collapse toauto, read the natural content height, then clamp it, all inside the nativeinputevent handler.- Computed line-height cache —
minRows/maxRowsare row counts, not pixels; the real line-height is read once fromgetComputedStyleand cached in a ref so it adapts to whatever font size the consumer'sclassNamesets. - Controlled re-measure — programmatic
valuechanges don't fire aninputevent, so auseLayoutEffectkeyed onvaluere-runs the same measurement (DOM writes only, no state).