Textarea
Multi-line text field that auto-grows via `field-sizing: content`, with the same token-driven focus and error states as Input.
Overview
Textarea is a thin styled wrapper around the native <textarea>. It uses field-sizing: content so the field grows automatically as the user types - no rows juggling, no JS height measurement, no resize handle dancing.
Visual states match Input exactly: border-input for default, focus-visible:ring-ring/50 for focus, aria-invalid:border-destructive for error. The minimum height is min-h-16 so empty textareas always look like multi-line surfaces.
Preview
'use client';import { Textarea } from '@gremorie/rx-forms';export function TextareaPreview() { return ( <div className="max-w-md"> <Textarea placeholder="Write a description..." rows={4} /> </div> );}Anatomy
Textarea single styled <textarea>, min-h-16, auto-growing via field-sizing: contentInstallation
bash npx gremorie@latest add rx-textarea bash pnpm dlx gremorie@latest add rx-textarea bash yarn dlx gremorie@latest add rx-textarea bash bunx --bun gremorie@latest add rx-textarea Usage
import { Label, Textarea } from "@gremorie/rx-forms";
export function Example() {
return (
<div className="grid gap-2">
<Label htmlFor="bio">Bio</Label>
<Textarea id="bio" placeholder="Tell us about yourself..." />
</div>
);
}The Angular edition of this component ships from source today (see the workbench for the side-by-side); its registry entry is coming next.
API
<Textarea>
| Prop | Type | Default | Description |
|---|---|---|---|
rows | number | - | Hint for initial visible rows. Less critical here since field-sizing: content overrides static sizing once the user types. |
disabled | boolean | false | Disables interaction. Applies cursor-not-allowed and opacity-50. |
aria-invalid | boolean | - | When true, switches border and focus ring to the destructive token. |
Extends all React.ComponentProps<"textarea">. Renders with data-slot="textarea".
Auto-grow via field-sizing: content is supported in Chromium 123+ and
Firefox 128+. For older browsers, the textarea still works but does not
auto-resize; pass rows to set a reasonable static height.
Composition
<Textarea>is a leaf primitive. Pair with<Label>for accessibility.- For multiline composers with toolbars or submit buttons, wrap with
<InputGroup>and use<InputGroupTextarea>instead, then add an<InputGroupAddon align="block-end">for the footer. - For react-hook-form, wrap with
<FormField>+<FormControl>to auto-wire ARIA relationships.
Variations
With label
The canonical comment-style textarea. Pair it with a <Label> linked via htmlFor and add a helper description when the expected length matters.
'use client';import { Label, Textarea } from '@gremorie/rx-forms';export function TextareaWithLabelPreview() { return ( <div className="grid w-full max-w-sm gap-2"> <Label htmlFor="message">Message</Label> <Textarea id="message" placeholder="Type your message here." /> </div> );}Invalid state
aria-invalid switches the surface to destructive. Pair with aria-describedby pointing at the error so screen readers announce it.
Bio is required.
'use client';import { Label, Textarea } from '@gremorie/rx-forms';export function TextareaInvalidPreview() { return ( <div className="grid w-full max-w-sm gap-2"> <Label htmlFor="bio">Bio</Label> <Textarea id="bio" aria-invalid="true" aria-describedby="bio-error" defaultValue="" placeholder="Tell us about yourself." /> <p id="bio-error" className="text-sm text-destructive"> Bio is required. </p> </div> );}Disabled
disabled removes the field from the tab order and the form payload, drops opacity, and applies cursor-not-allowed. For visible-but-locked content prefer readOnly (see below).
'use client';import { Textarea } from '@gremorie/rx-forms';export function TextareaDisabledPreview() { return ( <div className="w-full max-w-sm"> <Textarea disabled placeholder="Disabled textarea" defaultValue="This field is read only." /> </div> );}Chat composer (with InputGroup)
For composer surfaces with a send button below, use InputGroup + InputGroupTextarea + align="block-end".
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from '@gremorie/rx-forms';
import { Send } from 'lucide-react';
<InputGroup>
<InputGroupTextarea placeholder="Write a message..." />
<InputGroupAddon align="block-end">
<InputGroupButton size="sm" variant="default" className="ml-auto">
<Send />
Send
</InputGroupButton>
</InputGroupAddon>
</InputGroup>;Read-only
Use readOnly (not disabled) when the content should be visible and selectable but not editable - e.g. displaying generated SQL or a saved note.
<div className="grid gap-2">
<Label htmlFor="generated-sql">Generated SQL</Label>
<Textarea
id="generated-sql"
readOnly
value="SELECT * FROM users WHERE created_at > '2026-01-01';"
/>
</div>Accessibility
- Labels: every textarea needs an associated
<Label>viahtmlFormatching the textarea'sid, or an explicitaria-label. - Focus: 3px focus-visible ring driven by
focus-visible:ring-ring/50- only appears for keyboard users. - Validation: combine
aria-invalid="true"witharia-describedbypointing at the error message. - Disabled vs read-only:
disabledremoves the field from the tab order and the form payload.readOnlykeeps it tabbable, selectable, and submittable - prefer it for display-only content. - Auto-grow:
field-sizing: contentkeeps the cursor visible without manual scrolling once the textarea exceeds the viewport.
Related
- Input - single-line counterpart
- Label - the canonical companion
- Input Group - wrap with footer toolbar for composers
- Form - wires Textarea into react-hook-form with ARIA helpers