Textarea
Multi-line text that grows with its content, with a counter, a footer and an error as text.
One of 9 components in Forms and inputs
Details
What Textarea brings along, read from the registry and the source.
- Category
- Forms and inputs
- Area
- Elements
- Variants
- 2
- Version
- 1.0.0 · since 1.0.0
- File
- components/ui/textarea.tsx
- Brings along
- utilskitinput
- npm packages
- None
- Languages
- English and German, 2 texts of its own
- Studio
- Adjust in code
- Build with AI
- Over MCP in Claude, Cursor, Copilot and Codex
Props
The TextareaProps interface from components/ui/textarea.tsx, with the comments from the code. Also takes every prop of FieldOwnProps, Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'maxLength'>.
autoResizeoptionalbooleanGrows with its content between minRows and maxRows (CSS field-sizing, falls back to rows)
minRowsoptionalnumbermaxRowsoptionalnumbermaxLengthoptionalnumberShows a counter. Going over is allowed while typing and shown as an error, so nothing gets cut off silently.
footeroptionalReact.ReactNodeSomething in the footer row, e.g. formatting hints or an attach button
invalidoptionalbooleanrefoptionalReact.Ref<HTMLTextAreaElement>
Built-in texts
Textarea writes these texts itself. The language follows the KitProvider, the labels prop replaces single ones.
Show all 2 texts
| Key | English | German |
|---|---|---|
| left | English characters left | German Zeichen übrig |
| over | English characters too many | German Zeichen zu viel |
Accessibility
Read from the source of the component.
- Sets aria-describedby and aria-invalid, so screen readers announce purpose and state.
- Adds text for screen readers where icons and graphics stand alone.
- The server renders the starting state in full, JavaScript only adds the interaction.
Motion
- No animations, it stands still from the start.
Source
The file aiw add puts into your project. From then on it is yours.
Show the file, 86 lines
'use client';
import { useRef, useState } from 'react';
import { cn, useLocale, useTexts } from '@/lib/kit';
import { controlBox, useControl, useShake, withField, type FieldOwnProps } from '@/components/ui/input';
const TEXTS = {
en: { left: 'characters left', over: 'characters too many' },
de: { left: 'Zeichen übrig', over: 'Zeichen zu viel' },
};
export interface TextareaProps extends FieldOwnProps, Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'maxLength'> {
/** Grows with its content between minRows and maxRows (CSS field-sizing, falls back to rows) */
autoResize?: boolean;
minRows?: number;
maxRows?: number;
/** Shows a counter. Going over is allowed while typing and shown as an error, so nothing gets cut off silently. */
maxLength?: number;
/** Something in the footer row, e.g. formatting hints or an attach button */
footer?: React.ReactNode;
invalid?: boolean;
ref?: React.Ref<HTMLTextAreaElement>;
labels?: Partial<(typeof TEXTS)['en']>;
className?: string;
}
function TextareaBox({ autoResize = true, minRows = 3, maxRows = 10, maxLength, footer, invalid, labels, className, value, defaultValue, onChange, ref, ...rest }: Omit<TextareaProps, keyof FieldOwnProps>) {
const t = useTexts(TEXTS, labels);
const locale = useLocale();
const c = useControl({ ...rest, invalid });
const [inner, setInner] = useState(String(defaultValue ?? ''));
const text = value !== undefined ? String(value) : inner;
const over = maxLength !== undefined && text.length > maxLength;
const left = maxLength !== undefined ? maxLength - text.length : 0;
const fmt = (n: number) => n.toLocaleString(locale === 'de' ? 'de-DE' : 'en-US');
const showFooter = maxLength !== undefined || footer;
const boxRef = useRef<HTMLDivElement>(null);
// One shake when the text goes over the limit or the field turns invalid
useShake(boxRef, c.invalid || over);
return (
<div ref={boxRef} data-invalid={c.invalid || over || undefined} data-disabled={c.disabled || undefined} className={cn('flex w-full flex-col overflow-hidden rounded-lg', controlBox, className)}>
<textarea
{...rest}
ref={ref}
id={c.id}
value={value}
defaultValue={value === undefined ? defaultValue : undefined}
onChange={(e) => {
setInner(e.target.value);
onChange?.(e);
}}
disabled={c.disabled}
required={c.required}
aria-invalid={c.invalid || over || undefined}
aria-describedby={c['aria-describedby']}
rows={minRows}
style={{ minHeight: `calc(${minRows} * 1.5em + 1.25rem)`, maxHeight: `calc(${maxRows} * 1.5em + 1.25rem)` }}
className={cn(
'w-full resize-none bg-transparent px-3.5 py-2.5 text-base leading-[1.5] text-inherit outline-none placeholder:text-subtle-foreground disabled:cursor-not-allowed sm:text-sm sm:leading-[1.5]',
autoResize ? '[field-sizing:content]' : 'resize-y',
'kit-scrollbar',
)}
/>
{showFooter && (
<div className="flex min-h-10 items-center justify-between gap-3 px-2 pb-2 ps-3.5">
<div className="flex min-w-0 flex-1 items-center gap-1">{footer}</div>
{maxLength !== undefined && (
<p className={cn('ms-auto shrink-0 text-xs tabular-nums transition-colors duration-300', over ? 'font-medium text-destructive' : left <= maxLength * 0.1 ? 'text-foreground' : 'text-subtle-foreground')}>
<span aria-hidden>{over ? `−${fmt(-left)}` : `${fmt(text.length)} / ${fmt(maxLength)}`}</span>
<span className="sr-only">{over ? `${fmt(-left)} ${t.over}` : `${fmt(left)} ${t.left}`}</span>
</p>
)}
</div>
)}
</div>
);
}
/**
* Multi-line text that grows with its content, with an optional counter and footer.
* <Textarea label="Message" maxLength={500} hint="We reply within a day." />
*/
export function Textarea({ label, hint, error, optional, labelAction, className, ...rest }: TextareaProps) {
const wrapped = Boolean(label || hint || error);
return withField({ label, hint, error, optional, labelAction, id: rest.id, required: rest.required, disabled: rest.disabled, className }, <TextareaBox {...rest} className={wrapped ? undefined : className} />);
}Frequent questions
How do I install Textarea?
With aiw add @ai-website/textarea. The command puts components/ui/textarea.tsx into your project, the code is yours from then on. It brings along input. It needs no further npm packages.
Which variants does Textarea have?
2 variants, States and Composer.
Can I translate the texts of Textarea?
Yes. Textarea carries 2 texts of its own in English and German, such as “characters left” and “characters too many”. The language follows the KitProvider, single texts change through the labels prop.
Can an AI assistant build with Textarea?
Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Textarea with aiw_components and install it with aiw_add @ai-website/textarea.