A multi-line text input for comments, descriptions and messages. Comes as a complete field with label, help text, character count and validation, or as a bare styled textarea. Resizes vertically, stays fixed, or grows with its content.
import { TextareaField } from "@/components/ui/textarea";export default function TextareaDemo() { return ( <TextareaField className="w-full max-w-sm" label="Feedback" placeholder="What could we do better?" description="Your feedback goes to the product team." /> );}
import { Textarea, TextareaField } from "@/components/ui/textarea";
tsx
<TextareaField label="Description" name="description" placeholder="Describe the issue" />
The file exports two components:
TextareaField — a React Aria TextField with a label, description, character count and error message around a Textarea. Use it in forms.
Textarea — just the styled <textarea>. Use it when you lay out the label yourself, or inside a custom composer.
The two onChange signatures differ
TextareaField is a React Aria field, so onChange receives the new string:
onChange={setValue}. A standalone Textarea is a plain textarea element,
so onChange receives the event: onChange={(e) => setValue(e.target.value)}.
<TextareaField> {/* root: React Aria TextField, owns value and validation */} <Label /> {/* from `label` */} <Textarea /> {/* the native <textarea> */} <div> {/* only when there is a description or a count */} <Description /> {/* from `description` */} <span /> {/* `value.length / maxLength` when `showCount` */} </div> <FieldError /> {/* shown only while invalid */}</TextareaField>
Part
Renders
Notes
TextareaField
<div>
Root. Carries data-slot="textarea-field", the group/field class and all state attributes.
Label
<label>
Rendered when label is set. Adds a red * when isRequired.
Textarea
<textarea>
The control itself, styled by textareaVariants. Carries data-slot="textarea". Receives placeholder, rows, variant and resize from the field.
Description
<span slot="description">
Rendered when description is set. Linked with aria-describedby.
Count
<span>
Rendered when showCount and maxLength are both set. Right-aligned, tabular numbers.
outline for most forms, filled for dense layouts and tinted surfaces, underlined for minimal, inline editing. They match the variants of the other field components.
import { TextareaField } from "@/components/ui/textarea";export default function TextareaVariants() { return ( <div className="flex w-full max-w-sm flex-col gap-5"> <TextareaField variant="outline" label="Outline" placeholder="Default style for most forms" /> <TextareaField variant="filled" label="Filled" placeholder="Muted background for dense layouts" /> <TextareaField variant="underlined" label="Underlined" placeholder="Bottom border only" /> </div> );}
resize="vertical" (default) lets users drag the corner. none keeps a fixed height and scrolls. auto grows the textarea with its content using CSS field-sizing: content; in browsers without support it falls back to a fixed height. Use rows to set the initial height; the minimum is 80px.
Drag the corner to resize.
Fixed height; content scrolls.
Grows with its content.
import { TextareaField } from "@/components/ui/textarea";export default function TextareaResize() { return ( <div className="flex w-full max-w-sm flex-col gap-5"> <TextareaField label="Vertical (default)" rows={3} description="Drag the corner to resize." defaultValue="Users can make this taller or shorter." /> <TextareaField label="None" resize="none" rows={3} description="Fixed height; content scrolls." defaultValue="The height stays at three rows no matter what." /> <TextareaField label="Auto" resize="auto" description="Grows with its content." defaultValue={ "Type a few more lines here.\nThe field grows to fit them." } /> </div> );}
showCount shows value.length / maxLength under the field when maxLength is set. It works on controlled and uncontrolled fields: with value the count reads from it, otherwise the field tracks the length itself, starting from defaultValue. Your onChange is still called. maxLength also stops input at the limit.
Grows with its content.19/160
import { TextareaField } from "@/components/ui/textarea";export default function TextareaCount() { return ( <TextareaField className="w-full max-w-sm" label="Bio" placeholder="A few words about you" defaultValue="Designer in Lisbon." maxLength={160} showCount resize="auto" description="Grows with its content." /> );}
isDisabled dims the field and removes it from the tab order. isReadOnly keeps the text focusable and selectable, which suits quoting an original message.
Notes are locked once a ticket is closed.
import { TextareaField } from "@/components/ui/textarea";export default function TextareaDisabled() { return ( <div className="flex w-full max-w-sm flex-col gap-5"> <TextareaField label="Internal notes" isDisabled defaultValue="Customer asked for a refund on the annual plan." description="Notes are locked once a ticket is closed." /> <TextareaField label="Original message" isReadOnly resize="none" defaultValue="Hi team, the export button on the reports page returns a 500 error since this morning." /> </div> );}
isRequired, minLength and maxLength use the browser's constraint validation. With the default validationBehavior="native", the form won't submit while the field is invalid and the browser's message appears on submit.
import { Form } from "react-aria-components";import { Button } from "@/components/ui/button";import { TextareaField } from "@/components/ui/textarea";export default function TextareaValidation() { return ( <Form className="flex w-full max-w-sm flex-col gap-4" onSubmit={(e) => e.preventDefault()} > <TextareaField label="Steps to reproduce" name="steps" isRequired rows={4} placeholder={"1. Open the reports page\n2. Click Export\n3. …"} /> <Button type="submit" className="self-start"> File bug </Button> </Form> );}
validate receives the current value and returns an error string, or null when valid. With validationBehavior="aria" on the Form, errors appear live as the user types.
import { Form } from "react-aria-components";import { Button } from "@/components/ui/button";import { TextareaField } from "@/components/ui/textarea";const wordCount = (value: string) => value.trim() ? value.trim().split(/\s+/).length : 0;export default function TextareaCustomValidation() { return ( <Form className="flex w-full max-w-sm flex-col gap-4" validationBehavior="aria" onSubmit={(e) => e.preventDefault()} > <TextareaField label="Why are you cancelling?" name="reason" defaultValue="Too pricey" description="At least 5 words, so we can act on it." validate={(value) => { if (/https?:\/\//.test(value)) return "Links aren't allowed here."; const words = wordCount(value); return words > 0 && words < 5 ? `Add ${5 - words} more word${5 - words === 1 ? "" : "s"}.` : null; }} /> <Button type="submit" variant="solid" color="danger" className="self-start" > Cancel subscription </Button> </Form> );}
Build your own field from React Aria's TextField, the Label, Description and FieldError parts in field.tsx, and Textarea. Here the description sits above the input and the label row carries an "Optional" hint. Add group/field to the root so the label reacts to the disabled and required states.
Optional
Gate codes, where to leave the parcel, pets.
import { TextField } from "react-aria-components";import { Description, FieldError, Label } from "@/components/ui/field";import { Textarea } from "@/components/ui/textarea";export default function TextareaComposition() { return ( <TextField className="group/field flex w-full max-w-sm flex-col gap-1.5" name="instructions" maxLength={500} > <div className="flex items-baseline justify-between"> <Label>Delivery instructions</Label> <span className="text-muted-foreground text-xs">Optional</span> </div> <Description>Gate codes, where to leave the parcel, pets.</Description> <Textarea rows={3} resize="none" placeholder="Leave it with the front desk in building B." /> <FieldError /> </TextField> );}
The tailwind-variants function behind Textarea, with variant (outline, filled, underlined) and resize (none, vertical, auto). Use it to style a textarea from another library the same way.
className on TextareaField styles the root; on Textarea it styles the textarea and also accepts a function of its render state (isFocused, isHovered, isInvalid, isDisabled).
Accepts every native <textarea> attribute, plus the props of React Aria's TextArea. Inside a TextField, the field supplies the value, id and ARIA attributes.