Textarea
Auto-growing multi-line input with an optional character counter.
import { Textarea }from "@/components/ui"
With counter
0/200
Full source
The complete component, read from src/components/ui/textarea.tsx.
textarea.tsx
"use client";
import { useRef, useEffect, useId, type ChangeEvent } from "react";
import { cn } from "@/lib/utils";
import { Field, controlBase, controlClasses } from "./field";
interface TextareaProps {
id?: string;
label?: string;
hint?: string;
error?: string;
required?: boolean;
placeholder?: string;
maxLength?: number;
rows?: number;
value?: string;
onChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void;
}
export function Textarea({
id,
label,
hint,
error,
required,
placeholder,
maxLength,
rows = 3,
value,
onChange,
}: TextareaProps) {
const ref = useRef<HTMLTextAreaElement>(null);
const reactId = useId();
const areaId = id ?? reactId;
const messageId = `${areaId}-message`;
const describedBy = error || hint ? messageId : undefined;
useEffect(() => {
const el = ref.current;
if (!el) return;
el.style.height = "auto";
el.style.height = Math.min(el.scrollHeight, 220) + "px";
}, [value]);
const len = (value || "").length;
const counter = maxLength
? { text: `${len}/${maxLength}`, warn: len > maxLength * 0.9 }
: undefined;
return (
<Field
label={label}
hint={hint}
error={error}
required={required}
htmlFor={areaId}
counter={counter}
messageId={messageId}
>
<textarea
ref={ref}
id={areaId}
rows={rows}
placeholder={placeholder}
value={value}
onChange={onChange}
maxLength={maxLength}
aria-invalid={Boolean(error) || undefined}
aria-describedby={describedBy}
className={cn(controlBase, controlClasses(error), "resize-y px-3 py-2 leading-relaxed")}
/>
</Field>
);
}Dependencies
To use Textarea you also need the following. Copy these into your project alongside it.
Recommended folder structure
project structure
src/components/ui/ ├─ field.tsx └─ textarea.tsx src/lib/ └─ utils.ts