Input
Textarea
Displays a multi-line text input — patterns for autoresize and a live character counter.
Examples
Autoresize
Grows as the user types, up to a sensible maximum, without showing a scrollbar
until that cap is hit. Uses the CSS field-sizing: content property so no
JavaScript is required.
Uses CSS field-sizing: content to grow with the value.
"use client";
import * as React from "react";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
export function TextareaAutoresizeDemo() {
const id = React.useId();
const [value, setValue] = React.useState(
"Hey, this textarea grows as you type. Try adding more lines!",
);
return (
<div className="grid w-full gap-2">
<Label htmlFor={id}>Autoresize textarea</Label>
<Textarea
id={id}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Start typing to see me grow..."
rows={2}
className="field-sizing-content max-h-48 min-h-16 resize-none"
/>
</div>
);
}Notes:
rows={2}sets the initial collapsed height.max-h-48caps growth; after that the browser shows a scrollbar.resize-nonehides the manual resize handle, since it is no longer needed.field-sizing: contentis supported in Chromium and Firefox. For Safari, measurescrollHeightin auseLayoutEffectand setheightdirectly.
Character limit
Shows a live counter against a hard maximum. The counter updates on every
keystroke, shifts visual weight near the limit, and is announced to assistive
tech via aria-live="polite" linked by aria-describedby.
0 of 180 characters used.
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
const MAX_LENGTH = 180;
export function TextareaCharacterLimitDemo() {
const id = React.useId();
const counterId = `${id}-counter`;
const [value, setValue] = React.useState("");
const remaining = MAX_LENGTH - value.length;
const isAtLimit = remaining <= 0;
return (
<div className="grid w-full gap-2">
<Label htmlFor={id}>Message</Label>
<Textarea
id={id}
value={value}
onChange={(e) => setValue(e.target.value.slice(0, MAX_LENGTH))}
placeholder="Write a short message..."
maxLength={MAX_LENGTH}
aria-describedby={counterId}
className="field-sizing-content min-h-20 resize-none"
/>
<div
id={counterId}
aria-live="polite"
className={cn(
"text-muted-foreground text-right text-xs tabular-nums",
remaining <= 20 && "text-foreground",
isAtLimit && "text-destructive",
)}
>
<span className="sr-only">
{value.length} of {MAX_LENGTH} characters used.{" "}
</span>
<span aria-hidden="true">
{value.length}/{MAX_LENGTH}
</span>
</div>
</div>
);
}Notes:
- Use the native
maxLengthattribute and slice inonChange—maxLengthdoes not prevent every programmatic paste / IME path. - Show
used/max(42/180), not just "remaining". Users scan for the ceiling as much as the current value. - Switch color near the limit (last ~10%) and use the destructive color once the limit is reached.
- Put the long-form phrase in a visually-hidden span for screen readers and the short form for sighted users.