Components

TextArea

The canonical multi-line text input — the block sibling of <Input>. Same friendly field treatment as Input — a solid surface with a visible border; gold glow on focus, red on invalid, green on valid, dimmed when disabled — grown to multiple lines. Vertically resizable by default; pass autoGrow to size it to its content instead.

TextArea is the primitive. For the form unit — label + helper / error with auto-linked ARIA — compose with TextAreaField. For a single line, reach for <Input> instead.

States

The same five treatments as Input, on a taller box. Idle is a clean border on a solid surface; focus lights up gold with a soft glow; invalid turns red, valid turns green (each pairs with an icon — ADR-0017); disabled dims to a muted fill. The sixth tile shows autoGrow — type to watch it size to its content.

state: idle
Default. Solid surface, visible border, manually resizable from the bottom-right.
state: focus
Gold border + soft gold glow. Shown statically via the is-focused modifier.
state: invalid
Red border + red glow; aria-invalid set. Pair with an error message via TextAreaField.
state: valid
Green border; aria-invalid="false". Use after a positive check passes.
state: disabled
Dimmed: muted fill + faded value, resize off.
behavior: autoGrow
No resize handle, no scrollbar — the box grows to fit as you type.

In a form

Inside a form, reach for TextAreaField — the form-bound compound. It reads the field from TanStack Form context, maps touched + errors to the red border and an error HelperText, and wires the label / control / description ARIA. Same contract as TextField, multi-line.

What happened, and what you expected instead.

Anatomy

"Materiality" — content on glass, hairline frames, lift — is the proposed system's concept, and the tokens below document that anatomy. The official field keeps it flat and friendly: a solid, opaque surface, a visible border, and color doing the state work — gold glow on focus, red on error, green on valid.

body
Transparent, --space-px hairline in --border-hairline, --radius-sm corners.

That transparent, hairline-framed body is the proposed treatment. The official field is opaque — solid background, regular border, rounded corners. Min-height floors the box; rows sets the initial line count.

state frames
Focus --gold-aa-500, invalid --rust-500, valid --patina-500; each with the matching focus ring.

Color carries the state in both systems. Officially: gold for focus, red for errors, green for valid — each with a soft matching glow. State colors always pair with a HelperText icon, never color alone.

resize
Vertical by default; autoGrow switches to JS sizing (no handle, no scroll).

Horizontal resize is off — width belongs to the layout, not the user.

lift
--shadow-xs at rest, --shadow-sm on focus.

Lift is the proposed system's trick — official fields don't float. They sit flat on the page, and the focus glow does the talking.

Props

invalid?: boolean
Red border + aria-invalid. Wins over valid if both are set.
valid?: boolean
Green border + aria-invalid="false". Use after a positive validation passes.
autoGrow?: boolean
Grow to fit content instead of scrolling; disables the manual resize handle.
rows, placeholder, value, onChange, disabled, maxLength, …
Every native <textarea> attribute passes through. rows sets the initial height (default 4).
ref: Ref<HTMLTextAreaElement>
Forwarded to the underlying textarea (autoGrow reads scrollHeight from it).

Usage

Standalone — controlledtsx
import { TextArea } from '@halo-compliance/ui'

<TextArea
  value={notes}
  onChange={(e) => setNotes(e.target.value)}
  rows={4}
  placeholder="Add a note…"
/>
Auto-growingtsx
<TextArea value={notes} onChange={onChange} autoGrow rows={2} />
Form-bound — TextAreaFieldtsx
<form.AppField name="notes">
  {() => (
    <TextAreaField
      label="Describe the issue"
      helperText="What happened, and what you expected."
      autoGrow
    />
  )}
</form.AppField>

When to use

  • Free-form prose — a note, a description, a reason, a comment that may run several lines.
  • Unknown-length input — pair with autoGrow so the field follows the content instead of trapping it in a scroller.
  • A captured artifact — pasted logs, a rulebook excerpt, a block someone needs to read back.

Antipatterns

  • A single line. If it is one line — a name, an email, a number — that is Input. A one-row TextArea is a misshapen Input.
  • Rich text. Bold / links / lists need an editor, not a textarea. TextArea is plain text only.
  • A bare control. Always give it a label — compose TextAreaField, or wire your own <label> + aria-describedby.
  • /input — the single-line sibling; same look, with icon slots.
  • /text-field — the single-line form binding (TextArea’s TextAreaField mirrors it).
  • /form — the form layer TextAreaField plugs into.
  • /tokens — the --shadow-xs / --radius-sm / focus-ring tokens behind the field.