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

'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: content

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>
  );
}

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>

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

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> 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