Skip to main content
Gremorie

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>

PropTypeDefaultDescription
rowsnumber-Hint for initial visible rows. Less critical here since field-sizing: content overrides static sizing once the user types.
disabledbooleanfalseDisables interaction. Applies cursor-not-allowed and opacity-50.
aria-invalidboolean-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

  1. <Textarea> is a leaf primitive. Pair with <Label> for accessibility.
  2. 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.
  3. 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> via htmlFor matching the textarea's id, or an explicit aria-label.
  • Focus: 3px focus-visible ring driven by focus-visible:ring-ring/50 - only appears for keyboard users.
  • Validation: combine aria-invalid="true" with aria-describedby pointing at the error message.
  • Disabled vs read-only: disabled removes the field from the tab order and the form payload. readOnly keeps it tabbable, selectable, and submittable - prefer it for display-only content.
  • Auto-grow: field-sizing: content keeps the cursor visible without manual scrolling once the textarea exceeds the viewport.
  • 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

On this page