Skip to main content

Field Array

View as Markdown

A repeatable group of fields, for letting users add and remove rows like "add another person".

typo3 ui:add field-array

Read more about installing Components and Primitives.

A row's markup is authored once, inside itemTemplate - a <template> element that's never rendered directly, only cloned client-side whenever addTrigger is clicked. Existing rows are a separate, ordinary loop over data you already have, rendered for real:

<primitives:fieldArray.itemTemplate>
    <primitives:fieldArray.item>
        <!-- one row's fields, authored once -->
    </primitives:fieldArray.item>
</primitives:fieldArray.itemTemplate>

<primitives:fieldArray.itemGroup>
    <f:for each="{people}" as="person" iteration="it">
        <primitives:fieldArray.item index="{it.index}">
            <!-- the same row markup, authored again with this person's data -->
        </primitives:fieldArray.item>
    </f:for>
</primitives:fieldArray.itemGroup>

Yes, the row is written twice - once for the stencil, once for the loop. This isn't a shortcut taken for FieldArray specifically; it's the same pattern FileUpload already uses for mixing already-uploaded files with newly-picked ones (see its docs' "Editing" section), and it's what keeps every row genuinely server-rendered rather than reconstructed from JSON after the page loads.

Each fieldArray.item's index prop (omitted inside itemTemplate, where no real index exists yet) automatically prefixes every nested Field's name - <ui:field name="firstName"> inside row 1 of an array named people becomes people[1][firstName] without you writing that out yourself.

root takes a required itemCount prop - the number of rows you're about to render in the loop above. Nothing counts your rows for you (they're your own f:for loop, not a collection FieldArray owns), so pass it explicitly, e.g. itemCount="{people -> f:count()}". It's what lets emptyState/addTrigger/removeTrigger start in the correct hidden/disabled state in the server-rendered HTML itself, matching minItems/maxItems, rather than only correcting themselves once JavaScript hydrates.

<primitives:fieldArray.root name="people" itemCount="{people -> f:count()}">
    <!-- ... -->
</primitives:fieldArray.root>

A FieldArray with no rows yet - emptyState shows until the first row is added.

No people added yet.

minItems="1" disables removeTrigger once a single row remains; maxItems="3" disables addTrigger once three rows exist. The status text below the rows ("2 of 3 added") isn't a FieldArray feature by itself - it's a plain element the row markup authors itself ({ui:ref(name: 'status', context: 'fieldArray')}), kept in sync from onItemAdded/onItemRemoved the same way this example already mounts each row's own Field/Input.

1 of 3 added (2 remaining)

A complete Form wrapping a FieldArray of guests, each with a name/email pair - required fields and the email format are validated live as you type or blur, using a Zod schema passed straight to validation. z.array(z.object({...})) covers however many guest rows currently exist (added or removed) without FieldArray needing to know about validation at all - each issue's own path (e.g. ['guests', 0, 'email']) is matched back to the exact row's own field automatically. Submission is blocked until every row is valid.

The following tables cover the available props of the Fluid Primitives.

fieldArray.root

Contains every part of the field array. Renders a <div> element.

NameDescriptionRequiredDefault
namestring
The base field name for this array's rows, e.g. `people` - each row's nested fields are automatically prefixed with it and their row index, producing names like `people[0][firstName]`.
Yes
-
itemCountinteger
Number of rows rendered server-side, e.g. `itemCount=""`. Lets `emptyState`/`addTrigger`/`removeTrigger` reflect the correct hidden/disabled state before JavaScript hydrates, instead of only afterward.
Yes
-
minItemsinteger
Minimum number of rows required - once exactly this many remain, removing a row is disabled.
No
-
maxItemsinteger
Maximum number of rows allowed - once this many exist, adding a row is disabled.
No
-
translationsarray
Localized screen-reader announcements for adding/removing a row. A `%number%` placeholder is replaced with the row's 1-based position. Set an entry to `` to disable that announcement, or (client-side only, via the FieldArray constructor) pass a callback to build the message from the row's own field values instead. Use `f:translate` for per-template localization overrides when needed.
No
-
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
rootIdstring
The root ID of the component, used for hydration and identification.
No
-
idsarray
The IDs of of the component parts for composition.
No
[]
controlledboolean
If true, the component is meant to be initialized manually inside another component
No
false
classstring
The CSS class(es) to be applied to the component.
No
-
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]

fieldArray.itemTemplate

Wraps one row's markup, authored once and cloned client-side for each added row. Renders a <template> element - never visible itself.

fieldArray.itemGroup

Groups every row, existing and added. Renders a <div> element.

NameDescriptionRequiredDefault
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]

fieldArray.item

One row. Existing rows are authored directly with an index; added rows are cloned from itemTemplate, which omits it. Renders a <div> element.

NameDescriptionRequiredDefault
indexinteger
This row's position in the array, e.g. `0`. Required for a real row; omitted inside `itemTemplate`'s stencil, where the client fills in a real index on each clone.
No
-
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]

fieldArray.emptyState

Shown while itemGroup has no rows yet. Renders a <div> element.

NameDescriptionRequiredDefault
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]

fieldArray.addTrigger

Appends a new row, cloned from itemTemplate. Renders a <button> element.

NameDescriptionRequiredDefault
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]

fieldArray.removeTrigger

Removes its enclosing row and re-indexes every later row down by one. Renders a <button> element.

NameDescriptionRequiredDefault
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
<primitives:fieldArray.root itemCount="{items -> f:count()}">
    <primitives:fieldArray.itemTemplate>
        <primitives:fieldArray.item>
            <!-- Your row's fields here -->
            <primitives:fieldArray.removeTrigger />
        </primitives:fieldArray.item>
    </primitives:fieldArray.itemTemplate>
    <primitives:fieldArray.itemGroup>
        <f:for each="{items}" as="item">
            <primitives:fieldArray.item index="{...}">
                <!-- Your row's fields here -->
                <primitives:fieldArray.removeTrigger />
            </primitives:fieldArray.item>
        </f:for>
        <primitives:fieldArray.emptyState />
    </primitives:fieldArray.itemGroup>
    <primitives:fieldArray.addTrigger />
</primitives:fieldArray.root>