Skip to content
goodtabs UI
BrowseBrowse components

Auto textarea

A textarea that grows with its content between a minimum and a maximum, then scrolls.

npx shadcn@latest add https://ui.goodtabs.co/r/auto-textarea.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/auto-textarea

Two rows to start, grows to eight, then scrolls. No resize handle.

Single row that grows

Starts as one line and grows to four.

minRows of one, so it starts like an input and grows to four lines.

With a character count

143 / 280

Controlled value with maxLength and a mono count line under the field.

Usage

import { AutoTextarea } from "@/components/ui/auto-textarea"

<AutoTextarea placeholder="Leave a comment" minRows={2} maxRows={8} />

Props

PropTypeDefaultDescription
minRowsnumber2Rows shown before there is any content.
maxRowsnumber8Rows it grows to before it starts to scroll.

Source

auto-textarea/auto-textarea.tsx
"use client";

import * as React from "react";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";

type AutoTextareaProps = React.ComponentProps<typeof Textarea> & {
  /** Rows shown before there is any content. */
  minRows?: number;
  /** Rows the field grows to before it starts to scroll. */
  maxRows?: number;
};

/** Sets the height from scrollHeight, clamped between minRows and maxRows. */
function fit(el: HTMLTextAreaElement, minRows: number, maxRows: number) {
  const style = window.getComputedStyle(el);
  const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.5;
  const padding = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom);
  const border = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
  const min = lineHeight * minRows + padding + border;
  const max = lineHeight * maxRows + padding + border;

  el.style.height = "0px";
  const content = el.scrollHeight + border;
  const next = Math.min(Math.max(content, min), max);
  el.style.height = `${next}px`;
  el.style.overflowY = content > max ? "auto" : "hidden";
}

/**
 * A textarea that grows with its content between minRows and maxRows, then scrolls.
 * Measures scrollHeight on every change and when the controlled value changes.
 */
export function AutoTextarea({
  minRows = 2,
  maxRows = 8,
  className,
  onInput,
  value,
  ref,
  ...props
}: AutoTextareaProps) {
  const inner = React.useRef<HTMLTextAreaElement | null>(null);

  const setRef = React.useCallback(
    (node: HTMLTextAreaElement | null) => {
      inner.current = node;
      if (typeof ref === "function") return ref(node);
      if (ref) ref.current = node;
    },
    [ref],
  );

  // Measure after render: on mount, when the controlled value changes and when the limits change.
  React.useLayoutEffect(() => {
    if (inner.current) fit(inner.current, minRows, maxRows);
  }, [value, minRows, maxRows]);

  // Width changes reflow the text, so measure again when the element resizes horizontally.
  React.useEffect(() => {
    const el = inner.current;
    if (!el || typeof ResizeObserver === "undefined") return;
    let width = el.offsetWidth;
    const observer = new ResizeObserver(() => {
      if (el.offsetWidth === width) return;
      width = el.offsetWidth;
      fit(el, minRows, maxRows);
    });
    observer.observe(el);
    return () => observer.disconnect();
  }, [minRows, maxRows]);

  return (
    <Textarea
      ref={setRef}
      rows={minRows}
      value={value}
      onInput={(e) => {
        fit(e.currentTarget, minRows, maxRows);
        onInput?.(e);
      }}
      className={cn("field-sizing-fixed min-h-0 resize-none", className)}
      {...props}
    />
  );
}

For AI agents

Hand your agent this page, the markdown at /components/auto-textarea/prompt.md, or the Copy AI prompt button above.

When to use

  • Comments, notes and descriptions where the length is unknown
  • Chat and message fields that should start small and grow
  • Replacing a resize handle with something that just fits

When not to use

  • Long documents or code, where a fixed tall editor is better
  • Single-line values: use an Input
  • Layouts where the field must never change height, such as a table cell

Agent notes

  • Client component. It extends shadcn's Textarea, so value, onChange, maxLength and every textarea prop pass through
  • Height comes from scrollHeight, set as an inline style; it re-measures on input, when the controlled value changes and when the width changes
  • rows is set to minRows, so the first server render is already the right size
  • Set the line height with a text or leading class if you change the font size; the row maths reads it from computed style