Skip to content

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

Example
120 Market Street
48 Harbor Road

Delete item?

This action cannot be undone.

php
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:

php
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:

php
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:

php
Repeater::make('contacts')->schema([
    TextInput::make('name')->required(),
    TextInput::make('email')->email()->required(),
]);

The generated validation rules are:

php
[
    '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:

php
Repeater::make('guests')
    ->schema([
        TextInput::make('name')->required(),
    ])
    ->minItems(1)   // adds min:1 validation rule
    ->maxItems(3);  // adds max:3 validation rule

At 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.

Item 1
Item 2
Item 3

Delete item?

This action cannot be undone.

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:

php
Repeater::make('line_items')
    ->schema([...])
    ->default([
        ['description' => 'Implementation', 'quantity' => 1],
    ]);

Set the initial values used when a user adds an item:

php
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:

php
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:

php
Repeater::make('addresses')
    ->schema([...])
    ->itemLabel('street');

Addable and Deletable

Control whether users may add new items or delete existing ones:

php
// 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:

php
Repeater::make('sections')
    ->schema([...])
    ->collapsible();

Start Collapsed

Load all items in their collapsed state:

php
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

Delete item?

This action cannot be undone.

Add Button Text

Customize the "Add" button label:

php
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

php
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:

php
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

php
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.

vue
<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>
tsx
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.