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-fillresolves to a 30% mix of the input edge — the same look the raw class gave. - Under
data-contrast="high"it resolves totransparent, 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/30when extending a field. Reach forbg-input-fillso both palettes stay correct.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | string | — | Hint text shown while the field is empty. |
defaultValue | string | — | Initial value for uncontrolled usage. |
value | string | — | Controlled value — pair with onChange. |
rows | number | — | Forwarded 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. |
disabled | boolean | false | Disables input and reduces opacity to 50%. |
aria-invalid | boolean | 'true' | 'false' | — | Applies the destructive-ink border and the destructive focus ring to signal a validation error. |
className | string | — | Additional Tailwind classes to merge. |
...rest | React.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
Labelby matching itshtmlForto theidon the textarea, so screen readers announce what the field is for. - Set
aria-invalidwhen validation fails — it applies the destructive ring and is exposed to assistive technology. Pointaria-describedbyat your message element so the reason is announced too. - Focus shows a visible gold ring and swaps the border to
--primary-ink, matchingInputandSelectTriggerso 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.