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
Installation
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>
);
}Angular edition planned for Phase 5h. Star the repo to track progress.
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
Bio field with description
The canonical comment-style textarea. Include a helper description so users know the expected length.
<div className="grid gap-2">
<Label htmlFor="bio">Bio</Label>
<Textarea id="bio" placeholder="Tell us about yourself..." />
<p className="text-sm text-muted-foreground">
Max 280 characters. Markdown supported.
</p>
</div>Error state
aria-invalid switches the surface to destructive. Pair with aria-describedby pointing at the error so screen readers announce it.
<div className="grid gap-2">
<Label htmlFor="msg">Message</Label>
<Textarea
id="msg"
aria-invalid
aria-describedby="msg-error"
defaultValue="too short"
/>
<p id="msg-error" className="text-sm text-destructive">
Message must be at least 20 characters.
</p>
</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