Skip to content

Repeater

The repeater renders a repeatable group of fields. Each row is its own copy of the schema, and the field submits an array of row objects — one object per row, keyed by the child field names. Create one with Repeater::make() and define the row template with ->schema().

Repeater::make('items', 'Line items')
->schema([
TextInput::make('name', 'Name')->required(),
TextInput::make('qty', 'Qty')->rules(['numeric']),
])
->minItems(1)
->maxItems(5)
->reorderable()
->addLabel('Add line')
->defaultItems(1)

->schema() takes the array of fields that make up a single row. Each child is a normal field, so it keeps its own label, placeholder, default value, and rules. The example above submits as:

['items' => [
['rowId' => '9f3c…', 'name' => 'Widget', 'qty' => '3'],
['rowId' => 'a2b1…', 'name' => 'Gadget', 'qty' => '1'],
]]

Every row carries a stable UUID under the reserved rowId key: rows filled from stored data keep theirs, new rows get one when they are added, and it survives validation into your handle() data. Row schemas must not declare their own rowId field.

->minItems() and ->maxItems() bound how many rows the form accepts; both are enforced as array-level validation rules. ->defaultItems() sets how many empty rows a fresh form starts with (default 1).

Repeater::make('items', 'Line items')
->schema([TextInput::make('name', 'Name')])
->minItems(1)
->maxItems(5)
->defaultItems(2);

Rows are reorderable by default through up/down controls. Pass ->reorderable(false) to fix the order.

Repeater::make('items', 'Line items')
->schema([TextInput::make('name', 'Name')])
->reorderable(false);

Rows stack by default — each row is a full block of stacked fields. Call ->table() to lay rows out as a table instead: the schema fields become columns, a shared header of the field labels sits above the rows, and each cell renders the input alone (the field’s own label moves up into the header).

Repeater::make('items', 'Line items')
->schema([
TextInput::make('name', 'Name'),
TextInput::make('qty', 'Qty')->rules(['numeric']),
])
->table();

Reorder controls (↑↓) sit on the left of each row and the row actions on the right; once a row carries more than one action they collapse into a ⋯ menu. Reordering slides each row to its new position, and the table scrolls horizontally on narrow screens. The slide animation is skipped when the viewer prefers reduced motion.

Call ->resizableColumns() to let the user drag column borders to resize them; pass showIndicator: true to surface a drag handle on each border.

Repeater::make('items', 'Line items')
->schema([
TextInput::make('name', 'Name'),
TextInput::make('qty', 'Qty')->rules(['numeric']),
])
->table()
->resizableColumns(showIndicator: true);

Every row carries an action menu. By default it holds just Remove (hidden while the row is at minItems). Pass ->rowActions() to declare the menu yourself, including the built-in Duplicate that clones a row in place. A single action renders inline; two or more collapse into a ⋯ menu. The same menu renders in both the stacked and table layouts — reorder controls stay separate.

Repeater::make('items', 'Line items')
->schema([
TextInput::make('name', 'Name'),
TextInput::make('qty', 'Qty')->rules(['numeric']),
])
->rowActions([
RowAction::duplicate(),
RowAction::remove()->label('Delete'),
]);

RowAction::duplicate() and RowAction::remove() are the built-ins; chain ->label(), ->icon(), or ->danger() to customise each. Remove is hidden automatically while the row sits at minItems. Pass an empty array — ->rowActions([]) — to drop the menu entirely.

->addLabel() sets the text on the button that appends a row (default “Add”). ->itemLabel() sets a per-row heading shown above each row’s fields.

Repeater::make('items', 'Line items')
->schema([TextInput::make('name', 'Name')])
->addLabel('Add line')
->itemLabel('Line');

Each child field’s rules are registered per row — items.{index}.field — so a child marked ->required() is required in each row and an error points at the offending row. The array-level minItems/maxItems rules validate the number of rows independently of the per-row rules. See Validation for how field rules are resolved.

Conditional rules on a child field resolve against the other fields in the same row first, falling back to form-level values. So visibleWhen, requiredWhen, readOnlyWhen, and disabledWhen on a row field compare against its siblings, and each row evaluates independently — the same client-side and on the server. See Conditional fields for the condition DSL.

Repeater::make('items', 'Line items')
->schema([
Select::make('type', 'Type')->options(['Fixed' => 'fixed', 'Hourly' => 'hourly']),
NumberInput::make('hours', 'Hours')
->visibleWhen('type', 'hourly')
->requiredWhen('type', 'hourly'),
]);

Repeater shares label, required, disabled, read-only, and visibility options with every field — see Fields. For validation and conditional behavior, see Validation and Conditional fields.