Textarea
An auto-growing textarea with 3 variants, 3 sizes and 3 tones matching Input - the height follows the content via field-sizing.
import { Textarea } from "@/components/ui/textarea"
export default function TextareaDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-3">
<Textarea placeholder="Tell us what you are building…" />
<Textarea size="sm" tone="success" defaultValue="Small size, success tone." />
</div>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/textareaDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install class-variance-authority@^0.7.1Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/textarea.tsximport type * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const textareaVariants = cva(
"flex field-sizing-content w-full outline-none transition-[color,box-shadow] duration-(--motion-fast) placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-danger aria-invalid:ring-danger/20 dark:aria-invalid:ring-danger/40",
{
variants: {
variant: {
outline: "border border-field-border bg-transparent shadow-xs dark:bg-input/30",
soft: "border border-transparent bg-surface-3 focus-visible:bg-transparent dark:bg-input/50",
ghost: "border border-transparent bg-transparent hover:bg-surface-3 focus-visible:bg-transparent",
},
size: {
sm: "min-h-12 rounded-md px-2.5 py-1.5 text-sm",
md: "min-h-16 rounded-lg px-3 py-2 text-sm",
lg: "min-h-20 rounded-lg px-3.5 py-2.5 text-base md:text-sm",
},
tone: {
neutral: "",
success:
"border-success focus-visible:border-success focus-visible:ring-success/20 dark:focus-visible:ring-success/40",
danger:
"border-danger focus-visible:border-danger focus-visible:ring-danger/20 dark:focus-visible:ring-danger/40",
},
},
defaultVariants: { variant: "outline", size: "md", tone: "neutral" },
}
)
interface TextareaProps
extends React.ComponentProps<"textarea">,
VariantProps<typeof textareaVariants> {}
function Textarea({ className, variant, size, tone, ...props }: TextareaProps) {
return (
<textarea
data-slot="textarea"
data-variant={variant ?? "outline"}
data-tone={tone ?? "neutral"}
className={cn(textareaVariants({ variant, size, tone, className }))}
{...props}
/>
)
}
export { Textarea, textareaVariants, type TextareaProps }Manual installs skip the @ai2/tokens theme - add the token CSS from the theming guide or the surface colors will be missing.
Usage
import { Textarea } from "@/components/ui/textarea"
<Textarea placeholder="Tell us what you are building…" />All three axes are optional - omit them and you get the outline medium neutral textarea. It grows with its content by default, starting at a per-size minimum height.
Examples
Variants
The variants match Input one-to-one, so mixed forms stay visually consistent.
Sizes
Each size sets the minimum height, padding, radius and text scale; auto-grow still takes over once the content exceeds the floor.
Tones
The tone axis colors the border and focus ring with the success or danger token, giving controlled validation feedback without touching aria-invalid.
Auto-grow
Sizing is handled by CSS field-sizing-content - type into the preview above and watch the height follow.
Invalid state
Set aria-invalid on validation errors - the danger border and ring style is built in.
Disabled
The native disabled attribute halves the opacity and sets a not-allowed cursor. A following ai2 Label dims with it automatically.
Props
Textarea also accepts every native <textarea> prop.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "outline" | "soft" | "ghost" | "outline" | Visual style of the textarea, matching Input. |
size | "sm" | "md" | "lg" | "md" | Minimum height, padding, radius and typography scale, aligned with Input. |
tone | "neutral" | "success" | "danger" | "neutral" | Semantic validation color: success and danger recolor the border and focus ring. |
ai2 Textarea: an auto-growing textarea for React forms
The ai2 Textarea is a shadcn-compatible textarea component for React, built on the native <textarea> element and styled with Tailwind CSS v4. Its height follows the content automatically through the CSS field-sizing-content property, so multi-line inputs grow as users type, with no resize handle and no JavaScript measuring.
Because it ships through the shadcn registry format, you install it with one CLI command, an MCP agent, or a copy-paste, and the source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Textarea?
It is a single Textarea component with a variant axis of three styles: outline (default, bordered), soft (filled surface that clears on focus) and ghost (transparent until hover), plus a size axis (sm, md, lg) and a tone axis (neutral, success, danger). The anatomy matches shadcn/ui, so existing snippets and AI agents keep working; all three props are additive.
The variants, sizes and tones match the ai2 Input one-to-one, so mixed forms with single-line and multi-line fields stay visually consistent. Styling comes from the ai2 token file, which means borders, surfaces and the focus ring follow your theme in both light and dark mode from day one.
Why use it
- Auto-grow without JavaScript: field-sizing-content lets the browser size the textarea to its content, starting from a per-size minimum height (min-h-16 at the default md). There is no hidden mirror element, no resize observer and no layout jump.
- Visual parity with Input: outline, soft and ghost use the same borders, surfaces and focus treatment as the ai2 Input, and the size and tone axes line up too, so a form that mixes both reads as one system.
- Validation styling built in: Set aria-invalid and the danger border and ring appear automatically, with a stronger ring in dark mode. Accessibility state and visual state stay in sync by construction.
- Native element underneath: It renders a real textarea, so labels, forms, autofill, spellcheck, maxLength and every other platform behavior work exactly as the browser intends.
- Agent-readable metadata: The registry item describes the variant axis and intended use in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component, its dependencies and the @ai2/tokens theme to your project.
- Three axes via cva: 3 variants (outline, soft, ghost), 3 sizes (sm, md, lg) and 3 tones (neutral, success, danger) are defined in a class-variance-authority matrix, and the exported textareaVariants lets you reuse the exact styles elsewhere.
- Focus-visible ring: A 3px ring and ring-colored border appear on focus, driven by the shared ring token rather than a hardcoded color.
- Invalid state styling: aria-invalid switches the border and ring to the ai2 danger token, so error styling needs no extra class.
- Motion from tokens: The color and box-shadow transition reads --motion-fast from the shared token file, matching every other ai2 component.
- Data attributes for styling: The element exposes data-slot="textarea", data-variant and data-tone, so you can restyle specific variants from CSS without forking the component.
Production tips
- Always pair with a label: Placeholders disappear on input. Use the ai2 Field or Label component so the purpose of the textarea stays visible and screen readers announce it.
- Cap the growth for long input: Auto-grow is unbounded by default. For comment boxes that can get very long, add a max-h utility with overflow-y-auto so the field stops growing at a sane height.
- Use tone for controlled validation color: tone="success" and tone="danger" recolor the border and focus ring without touching semantics. For actual errors prefer aria-invalid, which also announces the state to screen readers.
- Drive aria-invalid from your form library: Wire aria-invalid to the field error state from react-hook-form or similar. You get correct semantics and the danger styling in one attribute.
- Pick the variant per surface: outline reads best on plain backgrounds, soft on busy pages that need quieter fields, ghost inside toolbars and inline-edit contexts.
- Set the starting height with rows: The native rows attribute still works and defines the initial height before content takes over, useful when you want a taller empty state than the size minimum.
Works with the rest of ai2
The textarea slots into the ai2 form stack. Wrap it in an ai2 Field for label, description and error wiring, or attach an ai2 Label directly. Use it next to the ai2 Input in the same form; the variants, sizes and tones match one-to-one.
Finish the flow with an ai2 Button to submit, and confirm the result with ai2 Sonner toasts. Everything shares one token source, so combinations stay visually consistent in both modes.