For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to any URL or sending an Accept: text/markdown header. An agent skill is available at /.well-known/agent-skills/site-skill.md.
1
Sponsor

useAutoResizeTextarea

Grow a composer to its content and scroll after its maximum height.

A message composer should give a short message little space and a longer draft enough room to edit. A fixed-height textarea either wastes room when empty or starts scrolling before the user can see their draft.

useAutoResizeTextarea adjusts the textarea's height to its content within a minimum and maximum. Once the maximum is reached, the textarea keeps that height and uses native scrolling, so a long message cannot push the rest of the interface away.

Growth has limits

Use it for chat composers, comments, and other short-to-medium free-form input. A large document editor may be better served by its own layout. The hook only sizes the native textarea; it does not handle submission, keyboard shortcuts, validation, or rich text.

Pass the controlled value when drafts can change programmatically. User input and width changes are observed too, because wrapping can change the required height without changing the text. For an imperative value change outside React, call resize().

Type several lines below, load a draft, then send it. The field should grow, switch to scrolling at its limit, and shrink again when cleared.

Preview

Grows from 64px to 180px, then scrolls natively.

Installation

The starter includes hooks/use-auto-resize-textarea.ts. Import the hook directly; no application-wide provider is required.

Usage

import { useAutoResizeTextarea } from "@/hooks/use-auto-resize-textarea";
 
const [value, setValue] = useState("");
const { ref, resize } = useAutoResizeTextarea({
  value,
  minHeight: 64,
  maxHeight: 180,
});
<textarea
  ref={ref}
  value={value}
  onChange={(event) => setValue(event.target.value)}
/>;

API Reference

APITypeBehavior / default
valuestring or undefinedPass controlled content to track programmatic changes
minHeightnumber44px
maxHeightnumber240px; never below minHeight
refCallback refAttach to a textarea
resize()FunctionRecalculate after an imperative content change

Behavior

Input events, controlled value updates, and width changes recalculate the height. The textarea uses border-box measurements, preserving borders and padding, and switches to native vertical overflow at the limit. The demo supports loading a draft, typing, sending, and resetting. Event listeners, observers, and applied inline sizing are cleaned up.

Implementation

"use client";

import { useCallback, useEffect, useState } from "react";

export const useAutoResizeTextarea = ({
  value,
  minHeight = 44,
  maxHeight = 240,
}: { value?: string; minHeight?: number; maxHeight?: number } = {}) => {
  const [element, setElement] = useState<HTMLTextAreaElement | null>(null);
  const resize = useCallback(() => {
    if (!element) {
      return;
    }
    const min = Math.max(0, minHeight);
    const max = Math.max(min, maxHeight);
    const computed = getComputedStyle(element);
    const border =
      Number.parseFloat(computed.borderTopWidth) +
      Number.parseFloat(computed.borderBottomWidth);
    element.style.height = "0px";
    const required = element.scrollHeight + border;
    element.style.height = `${Math.min(max, Math.max(min, required))}px`;
    element.style.overflowY = required > max ? "auto" : "hidden";
  }, [element, minHeight, maxHeight]);
  useEffect(() => {
    if (!element) {
      return;
    }
    const original = {
      boxSizing: element.style.boxSizing,
      height: element.style.height,
      overflowY: element.style.overflowY,
    };
    element.style.boxSizing = "border-box";
    let width = element.clientWidth;
    const observer = new ResizeObserver(() => {
      if (width !== element.clientWidth) {
        width = element.clientWidth;
        resize();
      }
    });
    observer.observe(element);
    element.addEventListener("input", resize);
    resize();
    return () => {
      observer.disconnect();
      element.removeEventListener("input", resize);
      Object.assign(element.style, original);
    };
  }, [element, resize]);
  useEffect(() => {
    resize();
  }, [value, resize]);
  return { ref: setElement, resize };
};