Textarea
View as MarkdownA multi-line text input that works with Field, with optional word count, live-region announcements, and submit-on-Enter.
<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>
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- nofield.controlwrapper needed - Auto-resizes to fit its content via CSS
field-sizing: content- no@zag-js/auto-resizeor JS involved, see the MDN docs for browser support - Optional
submitOnprop to submit the nearest form on Enter or Cmd/Ctrl+Enter instead of inserting a newline - Optional
transformcallback to sanitize/reformat the value as the user types, with cursor position preserved - Optional
wordCountpart rendering a translatable "42 / 250 characters" style counter, driven bymaxLength - Optional
liveRegionpart that announces word count updates to screen readers via @zag-js/live-region, debounced so it doesn't spam assistive tech on every keystroke
Installation
typo3 ui:add textarea
Please copy the files manually from GitHub into your project.
Read more about installing Components and Primitives.
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.
<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.
<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().
<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" />
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:
<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" />
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
The following tables cover the available props of the Fluid Primitives.
textarea.root
Provides shared textarea state and wraps all related parts. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
placeholder | stringPlaceholder text shown when the textarea is empty. | No | - |
autocomplete | stringHints 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. | No | - |
name | stringThe name attribute of the textarea. Useful for form submission. Inherited from a surrounding field when available. | No | - |
disabled | booleanWhether the textarea is disabled. Inherited from a surrounding field when available. | No | - |
invalid | booleanWhether the textarea value is invalid. Inherited from a surrounding field when available. | No | - |
required | booleanWhether the textarea is required. Inherited from a surrounding field when available. | No | - |
readOnly | booleanWhether the textarea is readonly. Inherited from a surrounding field when available. | No | - |
defaultValue | stringThe 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. | No | '' |
maxLength | integerThe maximum number of characters allowed. Also drives the wordCount part, e.g. '42 / 250 characters'. | No | - |
rows | integerThe number of visible text lines. | No | - |
submitOn | Enum\TextareaSubmitOnSubmits 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. | No | - |
translations | arraySpecifies 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. | No | - |
announceDebounce | integerMilliseconds to debounce word count live-region announcements by, so rapid typing doesn't spam assistive tech. Set to 0 to announce every change immediately. | No | 600 |
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
rootId | stringThe root ID of the component, used for hydration and identification. | No | - |
ids | arrayThe IDs of of the component parts for composition. | No | [] |
controlled | booleanIf true, the component is meant to be initialized manually inside another component | No | false |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
textarea.label
Labels the textarea. Renders a <label> element.
| Name | Description | Required | Default |
|---|---|---|---|
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
textarea.textarea
The editable textarea. Renders a <textarea> element.
| Name | Description | Required | Default |
|---|---|---|---|
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
textarea.wordCount
Displays the character count, e.g. '42 / 250 characters'. Renders a <span> element.
| Name | Description | Required | Default |
|---|---|---|---|
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
textarea.liveRegion
Announces word count updates to assistive technology. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
Anatomy
<primitives:textarea.root>
<primitives:textarea.label />
<primitives:textarea.textarea />
<primitives:textarea.wordCount />
<primitives:textarea.liveRegion />
</primitives:textarea.root>