Components

Textarea

A multi-line text input for longer free-form content like comments, descriptions, and feedback. It auto-grows to fit its content and supports labels, validation, and disabled states.

Basic usage

Drop in a Textarea with a placeholder — it inherits a sensible minimum height and grows as the user types. The growth is CSS-only, via field-sizing-content, so there is no resize observer and no ref to wire up.

With a label

Pair with a Label via matching htmlFor and id, and add helper text below for guidance. To open the field taller than its two-line floor, raise the floor with a min-h-* class — rows does nothing here, because field-sizing-content replaces the row-based preferred height with a content-based one.

Your feedback helps us improve.

Invalid state

Unlike Input, the textarea carries no error prop — it styles itself from aria-invalid alone and leaves the message to you (or to Field / @comitor/ui/form). Both the border and the message text use destructive-ink, the on-page step; destructive itself is a fill role and only appears here as the translucent focus ring.

Write at least 20 characters.

Disabled

Interaction is blocked by the native attribute, disabled. The look is two classes on the component itself: disabled:opacity-50, which dims the field, and disabled:cursor-not-allowed, which swaps the text caret for not-allowed. Nothing is being taken away: a textarea rests at cursor: text, never at a pointer — the base-layer rule that puts the hand back on buttons and labels does not name <textarea>.

The dark fill token

In dark mode the field paints a faint fill behind itself, because the input border alone is barely distinguishable from the card it sits on. That fill is not a hand-rolled bg-input/30 — it is its own token, dark:bg-input-fill, and the distinction matters:

  • In the default palette --input-fill resolves to a 30% mix of the input edge — the same look the raw class gave.
  • Under data-contrast="high" it resolves to transparent, because that palette's input edge is already a light grey; tinting it would wash out the placeholder and drop it below AA.
  • The upshot: never write bg-input/30 when extending a field. Reach for bg-input-fill so both palettes stay correct.

Props

PropTypeDefaultDescription
placeholderstringHint text shown while the field is empty.
defaultValuestringInitial value for uncontrolled usage.
valuestringControlled value — pair with onChange.
rowsnumberForwarded to the native element but has no effect on height: field-sizing-content overrides the rows-based preferred size. Set the resting height with a min-h-* class instead.
disabledbooleanfalseDisables input and reduces opacity to 50%.
aria-invalidboolean | 'true' | 'false'Applies the destructive-ink border and the destructive focus ring to signal a validation error.
classNamestringAdditional Tailwind classes to merge.
...restReact.ComponentProps<'textarea'>All native textarea props are forwarded. The exported prop type is TextareaProps.

Accessibility

  • Renders a native <textarea>, so keyboard focus, text selection, and multi-line editing work without extra wiring.
  • Always associate a Label by matching its htmlFor to the id on the textarea, so screen readers announce what the field is for.
  • Set aria-invalid when validation fails — it applies the destructive ring and is exposed to assistive technology. Point aria-describedby at your message element so the reason is announced too.
  • Focus shows a visible gold ring and swaps the border to --primary-ink, matching Input and SelectTrigger so every field in a form focuses the same way.
  • The disabled state uses the native attribute, so the field is correctly skipped during keyboard navigation.