For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Textarea

The Textarea component from this design system, built on Base UI.

Installation

$ pnpm dlx shadcn@latest add @cobalt/textarea

Sizes

default and sm match the same sizes of Input, so the two stack on one edge. rows sets the resting height.

Auto resize

autoResize grows the field one line at a time up to maxRows, then scrolls. The height is measured from zero on every change, so it shrinks again when text is deleted.

Character count

Pair maxLength with a counter next to the description. The browser enforces the limit.

States

Hover and focus fade the ring in. disabled and aria-invalid override both. Use resize="none" for a fixed height.

Options

  • variant="ghost" removes the fill, padding, radius and rings, for inline editing. It matches Input.

Source

"use client";

import { cn } from "cn";
import * as React from "react";

type TextareaProps = React.ComponentProps<"textarea"> & {
  autoResize?: boolean;
  maxRows?: number;
  resize?: "none" | "vertical";
  size?: "default" | "sm";
  /** `ghost` removes the fill, padding and rings, for inline editing. Matches Input. */
  variant?: "default" | "ghost";
};

function Textarea({
  className,
  rows = 3,
  autoResize = false,
  maxRows,
  resize = "vertical",
  size = "default",
  variant = "default",
  onInput,
  ref,
  ...props
}: TextareaProps) {
  const innerRef = React.useRef<HTMLTextAreaElement | null>(null);
  const fit = () => {
    const field = innerRef.current;
    if (!field) {
      return;
    }
    if (!autoResize) {
      field.style.height = "";
      field.style.maxHeight = "";
      field.style.overflowY = "";
      return;
    }
    const line =
      Number.parseFloat(window.getComputedStyle(field).lineHeight) || 20;
    const padding = 16;
    const ceiling = maxRows ? maxRows * line + padding : Infinity;
    field.style.height = "0px";
    const next = Math.max(field.scrollHeight, rows * line + padding);
    field.style.height = `${Math.min(next, ceiling)}px`;
    field.style.maxHeight = maxRows ? `${ceiling}px` : "";
    field.style.overflowY = next > ceiling ? "auto" : "hidden";
  };
  React.useLayoutEffect(fit);

  return (
    <textarea
      ref={(node) => {
        innerRef.current = node;
        if (typeof ref === "function") {
          ref(node);
        } else if (ref) {
          ref.current = node;
        }
      }}
      data-slot="textarea"
      data-variant={variant}
      rows={rows}
      className={cn(
        // cobalt-allow-focus-within -- text input
        "block w-full min-w-0 rounded-[10px] border-0 bg-[var(--surface-strong,var(--muted))] py-2 font-sans text-sm leading-5 text-foreground ring-2 ring-transparent ring-inset outline-none transition-[background-color,box-shadow,color] duration-150 ease-[ease] motion-reduce:transition-none placeholder:text-[color:var(--foreground-subtle,var(--muted-foreground))] hover:not-focus:not-disabled:not-aria-invalid:ring-[var(--border-hover,var(--border))] focus:not-disabled:not-aria-invalid:ring-[var(--foreground-subtle,var(--ring))] focus:placeholder:text-foreground disabled:cursor-not-allowed disabled:bg-[var(--input-disabled-surface,var(--muted))] disabled:text-[color:var(--input-disabled-foreground,var(--muted-foreground))] disabled:placeholder:text-[color:var(--input-disabled-foreground,var(--muted-foreground))] aria-invalid:bg-[var(--input-invalid-surface,color-mix(in_srgb,var(--destructive)_15%,transparent))] aria-invalid:placeholder:text-[color:var(--input-invalid-placeholder,var(--destructive))]",
        size === "sm" ? "px-2.5" : "px-3",
        variant === "ghost" &&
          // cobalt-allow-focus-within -- text input
          "rounded-none bg-transparent p-0 hover:not-focus:not-disabled:not-aria-invalid:ring-transparent focus:not-disabled:not-aria-invalid:ring-transparent disabled:bg-transparent focus:placeholder:text-[color:var(--foreground-subtle,var(--muted-foreground))]",
        autoResize || resize === "none" ? "resize-none" : "resize-y",
        className
      )}
      onInput={(event) => {
        fit();
        onInput?.(event);
      }}
      {...props}
    />
  );
}

export { Textarea };