# Textarea

**A multi-line text input that works with Field, with optional word count, live-region announcements, and submit-on-Enter.**

**Reference:** [View Source](https://github.com/jramke/fluid-primitives/tree/main/Resources/Private/Primitives/Textarea) · [Zag.js Docs](https://zagjs.com/components/react/textarea)

**Textarea.html**

```html
<ui:textarea.root class="max-w-74" placeholder="Type your message here...">
    <ui:textarea.label>Message</ui:textarea.label>
    <ui:textarea.textarea />
</ui:textarea.root>
```

**Textarea.ts**

```ts
import { mountAll } from 'fluid-primitives';
import { Textarea } from 'fluid-primitives/textarea';

mountAll('textarea', ({ props }) => {
    const textarea = new Textarea(props);
    textarea.init();
    return textarea;
});

```

## Features

- Works standalone or nested directly inside `ui:field.root` - no `field.control` wrapper needed
- Auto-resizes to fit its content via CSS `field-sizing: content` - no `@zag-js/auto-resize` or JS involved, see the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/CSS/field-sizing) for browser support
- Optional `submitOn` prop to submit the nearest form on Enter or Cmd/Ctrl+Enter instead of inserting a newline
- Optional `transform` callback to sanitize/reformat the value as the user types, with cursor position preserved
- Optional `wordCount` part rendering a translatable "42 / 250 characters" style counter, driven by `maxLength`
- Optional `liveRegion` part that announces word count updates to screen readers via [@zag-js/live-region](https://zagjs.com), debounced so it doesn't spam assistive tech on every keystroke

## Installation

```bash
typo3 ui:add textarea
```

Or copy the files manually from GitHub (https://github.com/jramke/fluid-primitives.com/tree/main/packages/docs/Resources/Private/Registry/Textarea) into your project.

Read more about installing [Components and Primitives](/docs/core-concepts/primitives.md).

## Examples

### With Field

Nest `ui:textarea.root` directly inside `ui:field.root` - it inherits `name`, `disabled`, `required`, `invalid` and `aria-describedby` automatically, the same way `ui:input`/`ui:select` do. Use the primitive's own `label` part (nested inside `root`) rather than `field.label` - it targets the right control automatically.

**Textarea.html**

```html
<ui:field.root class="max-w-74" name="bio" required="{true}">
    <ui:textarea.root placeholder="Tell us about yourself">
        <ui:textarea.label>Bio</ui:textarea.label>
        <ui:textarea.textarea />
    </ui:textarea.root>
    <ui:field.description>Shown on your public profile.</ui:field.description>
    <ui:field.error />
</ui:field.root>
```

### With Word Count

Pass `maxLength` and add the `wordCount`/`liveRegion` parts wherever you want them - they don't need to be direct siblings of `textarea`.

**Textarea.html**

```html
<ui:textarea.root class="max-w-74" maxLength="280" placeholder="Tell us about yourself">
    <ui:textarea.label>Bio</ui:textarea.label>
    <ui:textarea.textarea />
    <ui:textarea.wordCount />
    <ui:textarea.liveRegion />
</ui:textarea.root>
```

### Submit on Enter

Pass `submitOn="{f:constant(name: 'Jramke\FluidPrimitives\Enum\TextareaSubmitOn::ModEnter')}"` to submit the nearest `<form>` on Cmd/Ctrl+Enter instead of inserting a newline - plain Enter still inserts a newline. Use `TextareaSubmitOn::Enter` for the opposite: plain Enter submits, Shift+Enter inserts a newline. Wrap the textarea in `ui:form.root` (or any native `<form>`) for the submit to actually go anywhere - `submitOn` just calls `closest('form')?.requestSubmit()`.

**TextareaSubmitOnEnterExample.html**

```html
<ui:exposeToClient />

<form {ui:ref(name: 'form' )} class="w-full max-w-74">
    <ui:textarea.root submitOn="{f:constant(name: 'Jramke\FluidPrimitives\Enum\TextareaSubmitOn::ModEnter')}" placeholder="Press Cmd/Ctrl+Enter to submit...">
        <ui:textarea.label>Message</ui:textarea.label>
        <ui:textarea.textarea rows="3" />
    </ui:textarea.root>
</form>

<vite:asset entry="EXT:docs/Resources/Private/Components/TextareaSubmitOnEnterExample/TextareaSubmitOnEnterExample.entry.ts" />
```

**TextareaSubmitOnEnterExample.ts**

```ts
import { mountAll } from 'fluid-primitives';

mountAll('textareaSubmitOnEnterExample', ({ createHydrator }) => {
    const hydrator = createHydrator();
    const form = hydrator.getElement<HTMLFormElement>('form');
    if (!form) return;

    form.addEventListener('submit', event => {
        event.preventDefault();
        alert('Form submitted');
    });
});

```

### With a Transform Callback

`transform` runs on every native `input` event, before the value is committed - return the value that should actually be written back to the textarea, with cursor position preserved across the rewrite. Because a real function can't cross the PHP → client JSON boundary, this can only be set by constructing `Textarea` yourself in a custom entry file, rather than as a Fluid prop. Type lowercase below - it's uppercased as you type:

**TextareaTransformExample.html**

```html
<ui:exposeToClient />

<ui:textarea.root class="max-w-74" controlled="{true}" rootId="transform-example-textarea" placeholder="Type in lowercase...">
    <ui:textarea.label>Shout Mode</ui:textarea.label>
    <ui:textarea.textarea />
</ui:textarea.root>

<vite:asset entry="EXT:docs/Resources/Private/Components/TextareaTransformExample/TextareaTransformExample.entry.ts" />
```

**TextareaTransformExample.ts**

```ts
import { mount, mountAll } from 'fluid-primitives';
import { Textarea } from 'fluid-primitives/textarea';

mountAll('textareaTransformExample', () => {
    mount('textarea', 'transform-example-textarea', ({ props }) => {
        const textarea = new Textarea({
            ...props,
            transform: value => value.toUpperCase(),
        });

        textarea.init();
        return textarea;
    });
});

```

## API Reference

### textarea.root

Provides shared textarea state and wraps all related parts. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `placeholder` | `string` | No | `-` | Placeholder text shown when the textarea is empty. |
| `autocomplete` | `string` | No | `-` | Hints for browser autofill, e.g. street-address. See the [HTML spec](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill) for the full list of tokens. |
| `name` | `string` | No | `-` | The name attribute of the textarea. Useful for form submission. Inherited from a surrounding field when available. |
| `disabled` | `boolean` | No | `-` | Whether the textarea is disabled. Inherited from a surrounding field when available. |
| `invalid` | `boolean` | No | `-` | Whether the textarea value is invalid. Inherited from a surrounding field when available. |
| `required` | `boolean` | No | `-` | Whether the textarea is required. Inherited from a surrounding field when available. |
| `readOnly` | `boolean` | No | `-` | Whether the textarea is readonly. Inherited from a surrounding field when available. |
| `defaultValue` | `string` | No | `''` | The initial value of the textarea when rendered. Use when you don't need to control the value of the textarea. Inherited from a surrounding field when available. |
| `maxLength` | `integer` | No | `-` | The maximum number of characters allowed. Also drives the wordCount part, e.g. '42 / 250 characters'. |
| `rows` | `integer` | No | `-` | The number of visible text lines. |
| `submitOn` | `Enum\TextareaSubmitOn` | No | `-` | Submits the nearest form on a keypress instead of inserting a newline: Enter submits on plain Enter (Shift+Enter still inserts a newline), ModEnter submits on Cmd/Ctrl+Enter (plain Enter always inserts a newline). Unset (the default) never intercepts Enter. |
| `translations` | `array` | No | `-` | Specifies the localized word count string. Set the wordCount entry to false to disable the wordCount part and live-region announcements entirely. Use `f:translate` for per-template localization overrides when needed. |
| `announceDebounce` | `integer` | No | `600` | Milliseconds to debounce word count live-region announcements by, so rapid typing doesn't spam assistive tech. Set to 0 to announce every change immediately. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `rootId` | `string` | No | `-` | The root ID of the component, used for hydration and identification. |
| `ids` | `array` | No | `[]` | The IDs of of the component parts for composition. |
| `controlled` | `boolean` | No | `false` | If true, the component is meant to be initialized manually inside another component |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

### textarea.label

Labels the textarea. Renders a `<label>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

### textarea.textarea

The editable textarea. Renders a `<textarea>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

### textarea.wordCount

Displays the character count, e.g. '42 / 250 characters'. Renders a `<span>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

### textarea.liveRegion

Announces word count updates to assistive technology. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

## Anatomy

```html
<primitives:textarea.root>
    <primitives:textarea.label />
    <primitives:textarea.textarea />
    <primitives:textarea.wordCount />
    <primitives:textarea.liveRegion />
</primitives:textarea.root>
```
