Repeater
The Repeater field lets users add, remove, and reorder groups of fields. Think of it as a dynamic array of sub-forms for line items, addresses, team members, or any list of structured data.
Basic Usage
use InertiaUI\Forms\Fields\Repeater;
use InertiaUI\Forms\Fields\TextInput;
Repeater::make('addresses')
->schema([
TextInput::make('street')->required(),
TextInput::make('city')->required(),
TextInput::make('zip')->required(),
])
->itemLabel('street')
->default([
['street' => '120 Market Street', 'city' => 'Riverton', 'zip' => '94016'],
['street' => '48 Harbor Road', 'city' => 'Lakeview', 'zip' => '94102'],
]);The value is stored as an array of objects. The outer value and every item are validated as arrays automatically.
Schema
The schema() method accepts an array of field definitions. Each item in the repeater gets its own instance of these fields:
use InertiaUI\Forms\Fields\Combobox;
Repeater::make('line_items')->schema([
TextInput::make('description')->required(),
TextInput::make('quantity')->number()->required(),
TextInput::make('unit_price')->number()->required(),
Combobox::make('tax_rate')->options([
'0' => '0%',
'0.09' => '9%',
'0.21' => '21%',
]),
]);You may also pass a closure if the schema depends on runtime data:
Repeater::make('items')->schema(function () {
return [
Combobox::make('product_id')->options(
Product::pluck('name', 'id')
),
TextInput::make('quantity')->number(),
];
});Nested Validation
Validation rules from the schema fields are automatically scoped to each item using Laravel's items.*.field syntax. This schema:
Repeater::make('contacts')->schema([
TextInput::make('name')->required(),
TextInput::make('email')->email()->required(),
]);The generated validation rules are:
[
'contacts' => ['array'],
'contacts.*' => ['array'],
'contacts.*.name' => ['required'],
'contacts.*.email' => ['email', 'required'],
]You do not need to set this up manually. It happens automatically.
Conditional visibility inside a row is evaluated with that row as local data and the complete form as root data. Hidden nested fields receive exclude rules, and unauthorized schema fields are omitted from serialization.
Min and Max Items
Limit the number of repeater items:
Repeater::make('guests')
->schema([
TextInput::make('name')->required(),
])
->minItems(1) // adds min:1 validation rule
->maxItems(3); // adds max:3 validation ruleAt maxItems, the "Add" button is hidden. At minItems, items may not be deleted below the minimum.
Limits must be zero or greater, and the minimum must not exceed the maximum. A maximum of 0 prevents items from being added. Passing null clears a configured limit.
Initial Values and New Item Defaults
Use the inherited default() method for the initial collection when no model value exists. It takes the complete array of rows:
Repeater::make('line_items')
->schema([...])
->default([
['description' => 'Implementation', 'quantity' => 1],
]);Set the initial values used when a user adds an item:
Repeater::make('line_items')
->schema([
TextInput::make('description'),
TextInput::make('quantity')->number(),
])
->defaultItem([
'description' => '',
'quantity' => 1,
]);Without defaultItem(), each named schema field starts as null. Pass an associative array whose keys match schema field names. defaultItem() affects newly added rows only. It does not merge into rows supplied by a model, default(), value, or defaultValue.
Reorderable
Drag-to-reorder is enabled by default. Disable it:
Repeater::make('steps')
->schema([...])
->reorderable(false);Item headers are numbered by position by default, so those numbers stay in order when rows move: reordering swaps the values, and the header still counts Item 1, Item 2, Item 3 down the page. On rows that look alike this reads as though nothing happened. Set itemLabel() to a schema field so each header carries its own row's value and visibly travels with it:
Repeater::make('addresses')
->schema([...])
->itemLabel('street');Addable and Deletable
Control whether users may add new items or delete existing ones:
// Read-only list: no adding or removing
Repeater::make('history')
->schema([...])
->addable(false)
->deletable(false);Collapsible
Allow items to be collapsed to save vertical space. This is especially useful for repeaters with many fields per item:
Repeater::make('sections')
->schema([...])
->collapsible();Start Collapsed
Load all items in their collapsed state:
use InertiaUI\Forms\Fields\Textarea;
use InertiaUI\Forms\Fields\TextInput;
Repeater::make('sections')
->schema([
TextInput::make('heading')->required(),
Textarea::make('body')->rows(3),
])
->itemLabel('heading')
->collapsed(); // implies collapsible()Calling collapsed() automatically enables collapsible.
Overview
Requirements
Add Button Text
Customize the "Add" button label:
Repeater::make('team_members')
->schema([...])
->addButtonText('Add Team Member');Without addButtonText(), the translated label uses itemLabel() when present and otherwise uses the translated word "Item".
Item Labels
Give each repeater item a label. This is shown in the item header, especially useful when items are collapsible:
Static Prefix
Repeater::make('addresses')
->schema([...])
->itemLabel('Address')
->collapsible();Items are labeled "Address 1", "Address 2", etc.
Field-Based Label
Pass a schema field name to use that field's current value as the item label:
Repeater::make('contacts')
->schema([
TextInput::make('name'),
TextInput::make('email'),
])
->itemLabel('name')
->collapsible();An itemLabel that matches a schema field uses that field's non-empty current value as the label. Numeric 0 and boolean false are valid labels. Otherwise the string is used as a prefix with the one-based position, such as name 2 or Address 2.
Invoice Line Items
Repeater::make('line_items')
->label('Invoice Items')
->help('Add at least one line item')
->schema([
TextInput::make('description')->required(),
TextInput::make('quantity')->number()->required(),
TextInput::make('unit_price')->number()->required(),
])
->minItems(1)
->maxItems(20)
->reorderable()
->collapsible()
->itemLabel('description')
->addButtonText('Add Line Item')
->required();Shared Field APIs
The repeater methods above compose with shared field APIs. You may use Form Class for labels and help text, Model Binding for default(), Validation for rules and required fields, Styling for classes and presentation helpers, Conditional Visibility, and Authorization.
Using the Component Directly
Repeater is exported from each package's components subpath. You may use it for standalone row collections or while rendering forms manually.
<script setup lang="ts">
import type { RepeaterField } from '@inertiaui/form-vue'
import { Repeater } from '@inertiaui/form-vue/components'
import { ref } from 'vue'
const schema: RepeaterField['schema'] = [
{
component: 'TextInput',
name: 'description',
label: 'Description',
inputType: 'text',
required: true,
precognitive: false,
disabled: false,
readonly: false,
autofocus: false,
clearable: false,
copyable: false,
viewable: false,
},
{
component: 'TextInput',
name: 'quantity',
label: 'Quantity',
inputType: 'number',
required: true,
precognitive: false,
disabled: false,
readonly: false,
autofocus: false,
clearable: false,
copyable: false,
viewable: false,
},
]
const lineItems = ref<Record<string, unknown>[] | null>([
{ description: 'Implementation', quantity: 1 },
])
</script>
<template>
<Repeater
v-model="lineItems"
id="line-items"
name="line_items"
label="Invoice Items"
help="Add the invoice rows."
:schema="schema"
:min-items="1"
:max-items="20"
:reorderable="true"
:addable="true"
:deletable="true"
:collapsible="true"
:collapsed="false"
item-label="description"
add-button-text="Add Line Item"
:default-item="{ description: '', quantity: 1 }"
/>
</template>import { useState } from 'react'
import type { RepeaterField } from '@inertiaui/form-react'
import { Repeater } from '@inertiaui/form-react/components'
const schema: RepeaterField['schema'] = [
{
component: 'TextInput',
name: 'description',
label: 'Description',
inputType: 'text',
required: true,
precognitive: false,
disabled: false,
readonly: false,
autofocus: false,
clearable: false,
copyable: false,
viewable: false,
},
{
component: 'TextInput',
name: 'quantity',
label: 'Quantity',
inputType: 'number',
required: true,
precognitive: false,
disabled: false,
readonly: false,
autofocus: false,
clearable: false,
copyable: false,
viewable: false,
},
]
export default function LineItems() {
const [lineItems, setLineItems] = useState<Record<string, unknown>[] | null>([
{ description: 'Implementation', quantity: 1 },
])
return (
<Repeater
value={lineItems}
onValueChange={setLineItems}
id="line-items"
name="line_items"
label="Invoice Items"
help="Add the invoice rows."
schema={schema}
minItems={1}
maxItems={20}
reorderable
addable
deletable
collapsible
collapsed={false}
itemLabel="description"
addButtonText="Add Line Item"
defaultItem={{ description: '', quantity: 1 }}
/>
)
}Native attributes and listeners belong on fields in schema; they target each field's underlying native element. See Native Attributes And Events.
Accessibility
Every row has keyboard move buttons in addition to pointer dragging, and reorder results are announced through a live status region. Focus follows a moved row. Collapsible rows use native details and summary semantics. Deleting opens a labeled confirmation dialog, keeps focus inside it, and restores focus when it closes. Newly added rows receive focus at their first child field.