# Text area

Package: @commenda/pathfinder@0.1.0
Page: /docs/components/text-area
Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=84-2

```tsx
import { TextArea } from '@commenda/pathfinder/text-area';

export const TextAreaDefault = () => (
  <TextArea
    label="Functional analysis"
    placeholder="e.g. What the entity does for the group and how it is paid."
    description="Shown in the transfer pricing report."
  />
);
```


## Installation [#installation]

Import the component in your screen:

```tsx
import { TextArea } from '@commenda/pathfinder/text-area';
```

Load the stylesheet once in your app root, after host styles:

```tsx
import '@commenda/pathfinder/styles.css';
```

Pathfinder ships precompiled CSS; no Tailwind setup is required. In Next.js App Router, put interactive state and callbacks in a client component. See [installation](/docs/getting-started/installation) for the workspace setup.

## API reference [#api-reference]

| Prop | Type | Requirement | Behavior |
| --- | --- | --- | --- |
| `resizeConfig` | `{ mode: "auto"; minHeight?: number; maxHeight: number; } \| { mode: "manual"; minHeight?: number; maxHeight?: number; } \| { mode: "fixed"; height: number; }` | optional | Auto 80–240px |
| `characterLimitConfig` | `{ max: number; }` | conditional | Soft grapheme limit; excess text is preserved |
| `maxLength` | `number` | optional | Native UTF-16 hard limit; mutually exclusive with soft limit |
| `actionConfig` | `CopyAction` | optional | Optional copy action |
| `label` | `string` | conditional | Visible field label. Supply an accessible name when it is omitted. |
| `hideLabel` | `boolean` | optional | Visually hide the label while preserving its accessible name. |
| `aria-label` | `string` | conditional | Accessible name when the field has no visible label. |
| `required` | `boolean` | conditional | Mark the field as required. See validation guidance for the component. |
| `labelConfig` | `{ optionalLabel: string; help?: never; } \| { help: string; optionalLabel?: never; }` | optional | Choose either `optionalLabel` or `help`, never both. `never` excludes the other field. |
| `sourceConfig` | `FieldSourceConfig` | optional |  |
| `description` | `string` | optional | Supporting guidance associated with the field. |
| `errorConfig` | `{ message: string; reveal?: "after-blur" \| "immediate"; }` | optional | Validation message and when it becomes visible. |
| `disableConfig` | `DisableConfig` | optional | Disabled state with an optional explanation. |
| `value` | `string` | conditional |  |
| `onValueChange` | `((value: string) => void)` | conditional |  |
| `defaultValue` | `string` | optional |  |
| `variant` | `"plain" \| "default"` | optional | Plain appearance is for embedding in a composite control such as Thread. |

### resizeConfig fields

Choose one shape; excluded fields cannot be combined.

#### Option 1

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `mode` | `"auto"` | Yes |  |
| `minHeight` | `number` | No |  |
| `maxHeight` | `number` | Yes |  |

#### Option 2

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `mode` | `"manual"` | Yes |  |
| `minHeight` | `number` | No |  |
| `maxHeight` | `number` | No |  |

#### Option 3

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `mode` | `"fixed"` | Yes |  |
| `height` | `number` | Yes |  |

### characterLimitConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `max` | `number` | Yes |  |

### actionConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | `"copy"` | Yes |  |
| `label` | `string` | No |  |
| `onCopyError` | `((error: Error) => void)` | No |  |

### labelConfig fields

Choose one shape; excluded fields cannot be combined.

#### Option 1

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `optionalLabel` | `string` | Yes |  |

Do not supply: `help`.

#### Option 2

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `help` | `string` | Yes |  |

Do not supply: `optionalLabel`.

### sourceConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | `"entity" \| "record" \| "document"` | Yes |  |
| `label` | `string` | Yes |  |
| `preview` | `ReactNode` | Yes |  |
| `onOpen` | `() => void` | Yes |  |
| `side` | `"top" \| "bottom"` | No |  |

### errorConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `message` | `string` | Yes |  |
| `reveal` | `"after-blur" \| "immediate"` | No |  |

### disableConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `isDisabled` | `boolean` | Yes |  |
| `reason` | `string` | No |  |

## Shared field props [#shared-field-props]

| Prop            | Type                                                | Behavior                                                     |
| --------------- | --------------------------------------------------- | ------------------------------------------------------------ |
| `label`         | `string`                                            | Visible accessible label; hideLabel hides it visually.       |
| `aria-label`    | `string`                                            | Accessible name when a visible label is not supplied.        |
| `value`         | `string`                                            | Controlled value; provide the corresponding change callback. |
| `defaultValue`  | `string`                                            | Initial value for an uncontrolled control.                   |
| `onValueChange` | `(value: string) => void`                           | Immediate editing changes; no implicit parsing.              |
| `description`   | `string`                                            | Visible supporting guidance.                                 |
| `required`      | `boolean`                                           | Native required semantics and visual asterisk.               |
| `labelConfig`   | `{ optionalLabel }` or `{ help }`                   | One annotation; cannot combine with required.                |
| `errorConfig`   | `{ message, reveal?: 'after-blur' \| 'immediate' }` | Replaces guidance after blur by default.                     |
| `disableConfig` | `{ isDisabled, reason? }`                           | Disables editing; reason stays visible.                      |
| `readOnly`      | `boolean`                                           | Preserves focus, selection, and form submission.             |

Native `name`, `form`, autocomplete, focus/blur handlers, constraints, IDs, ARIA attributes, and typed refs are supported. `className` styles the outer field. Use `onValueChange` for text changes. Form reset restores uncontrolled defaults; controlled values remain application-owned.

## Usage rules [#usage-rules]

Minimum authored height is 80px with Copy 13 text. Auto mode grows until its maximum and then scrolls; manual mode resizes vertically. Soft counters count grapheme clusters including emoji and report excess immediately. Hard native limits use browser UTF-16 counting. A composer/send action belongs outside this component.

Use `variant="plain"` inside a composite control such as Thread. It removes the standalone border, background and padding while preserving the accessible label, keyboard focus indicator and field behavior. Set `resizeConfig` to fit the containing control; Thread uses an initial height of 24px and a maximum of 160px.

### Limit [#limit]


```tsx
import { TextArea } from '@commenda/pathfinder/text-area';

export const TextAreaLimit = () => (
  <TextArea
    label="Summary"
    characterLimitConfig={{ max: 100 }}
    description="Keep the summary concise."
  />
);
```


### Resize [#resize]


```tsx
import { TextArea } from '@commenda/pathfinder/text-area';

export const TextAreaResize = () => (
  <div style={{ display: 'grid', gap: 20 }}>
    <TextArea
      label="Auto-growing notes"
      resizeConfig={{ mode: 'auto', maxHeight: 160 }}
    />
    <TextArea label="Resizable notes" resizeConfig={{ mode: 'manual' }} />
    <TextArea
      label="Fixed notes"
      resizeConfig={{ mode: 'fixed', height: 100 }}
    />
  </div>
);
```


### States [#states]


```tsx
import { TextArea } from '@commenda/pathfinder/text-area';

export const TextAreaStates = () => (
  <div style={{ display: 'grid', gap: 20 }}>
    <TextArea
      label="Report text"
      defaultValue="The entity provides management services."
      readOnly
      actionConfig={{ kind: 'copy' }}
    />
    <TextArea
      label="Notes"
      disableConfig={{ isDisabled: true, reason: 'This report is locked.' }}
    />
    <TextArea
      label="Description"
      errorConfig={{
        message: 'Describe the entity’s activities.',
        reveal: 'immediate',
      }}
    />
  </div>
);
```


## Design rules [#design-rules]

Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references.

### Source rules [#source-rules]

| Rule         | Source           | Contract                                                                  |
| ------------ | ---------------- | ------------------------------------------------------------------------- |
| TEXTAREA-001 | 84:363, 159:383  | Minimum 80px, Copy 13, grow to maximum then scroll.                       |
| TEXTAREA-002 | 159:146, 159:324 | Soft limit permits excess and reports how much; counter opposite support. |
| TEXTAREA-003 | 159:374          | Sending is composer behavior, not TextArea behavior.                      |

All source IDs refer to [The System](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=68-2).

### Explicit implementation decisions [#explicit-implementation-decisions]

True native readOnly remains distinct from disabled despite Figma 158:822 using disabled appearance for non-editable fields. Disabled values do not submit; read-only values do. Disabled editing does not disable the independent copy/reveal actions. Clear cannot modify a disabled/read-only field.

The auto-grow maximum of 240px is a configurable implementation default, not an authored Figma measurement. Soft counters use Unicode graphemes; native maxLength uses browser UTF-16 counting. They are mutually exclusive to avoid contradictory limits.

Error reveal defaults to after-blur; immediate reveal supports submission/server errors. Soft-limit feedback is immediate. Applications own validation schemas and server error clearing. Additional required/help guidance belongs in description when the single label annotation is already occupied.

Optional-label text uses pearl 800 instead of the authored pearl 700: browser axe measured the latter at 3.38:1 on white, below the 4.5:1 requirement for this text.
