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' => [['name' => 'Widget', 'qty' => '3'], ['name' => 'Gadget', 'qty' => '1']]]

->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 ->destructive() 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 apply to every row through the items.*.field wildcard, 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'),
]);

These are follow-ups, not part of the first release:

  • Nested repeaters (a repeater inside a repeater row).
  • Relationship auto-save (persisting rows straight to a related model).

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.