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…

"use client"

import * as React from "react"
import { cn } from "@/lib/utils"

/** SSR-safe: `CSS` doesn't exist in Node, so this stays `false` until it's read on the client. */
const supportsFieldSizing =
  typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("field-sizing", "content")

export interface AutosizeTextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
  /** Rows visible before the textarea starts growing. */
  minRows?: number
  /** Rows it grows to before switching to internal scrolling. */
  maxRows?: number

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/autosize-textarea.json

Prompt

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.
  • scrollHeight fallback — older browsers get a classic measure-and-set: collapse to auto, read the natural content height, then clamp it, all inside the native input event handler.
  • Computed line-height cacheminRows/maxRows are row counts, not pixels; the real line-height is read once from getComputedStyle and cached in a ref so it adapts to whatever font size the consumer's className sets.
  • Controlled re-measure — programmatic value changes don't fire an input event, so a useLayoutEffect keyed on value re-runs the same measurement (DOM writes only, no state).

On This Page