# Field: Repeater (category: special)

URL: https://docs.forms.saastro.io/docs/repeater

<FieldPreview
  name="repeater"
  description="Interactive demo of a repeater field — add and remove team members"
/>

## Overview

The repeater renders a dynamic list of items. Each item is a bordered card containing the sub-fields you declare in `itemFields`; users append new items with the add button and delete existing ones with the remove button. Internally the field is powered by `react-hook-form`'s `useFieldArray`, and its value is an **array of objects** — one object per item.

Typical uses: contact lists, order line items, multiple addresses, team members, "add another" sections of any kind.

## Usage

### FormBuilder API

```tsx

const config = FormBuilder.create('order')
  .addField('items', (f) =>
    f
      .type('repeater')
      .label('Order Items')
      .itemFields({
        product: { type: 'text', label: 'Product', placeholder: 'Product name' },
        quantity: { type: 'number', label: 'Quantity', placeholder: '1' },
      })
      .minItems(1)
      .maxItems(10)
      .addLabel('Add item')
      .removeLabel('Remove')
      .value([{ product: '', quantity: '' }]),
  )
  .addStep('main', ['items'])
  .build();
```

### JSON Configuration

```json
{
  "type": "repeater",
  "label": "Order Items",
  "itemFields": {
    "product": { "type": "text", "label": "Product", "placeholder": "Product name" },
    "quantity": { "type": "number", "label": "Quantity", "placeholder": "1" }
  },
  "minItems": 1,
  "maxItems": 10,
  "addLabel": "Add item",
  "removeLabel": "Remove"
}
```

## Value Shape

A repeater named `items` submits an array of objects, one per item, keyed by the `itemFields` names:

```ts
// Submitted data:
{
  items: [
    { product: 'Keyboard', quantity: '2' },
    { product: 'Mouse', quantity: '1' },
  ]
}
```

> `number` sub-fields hold **string** values at runtime, like the standalone [number input](/docs/text#number-type). Convert in your submit handler if you need numbers.

### Sub-field naming

Each rendered input is registered with `react-hook-form` under the path `{name}.{index}.{subName}` — the standard `useFieldArray` convention:

```
items.0.product
items.0.quantity
items.1.product
items.1.quantity
```

You'll see these paths when inspecting form state or reading values through `react-hook-form` APIs.

### Initial and default values

- The repeater's default value is `[]` — **the list starts empty** unless you set `value` to an array of item objects (as in the examples above).
- When the user clicks the add button, the new item initializes each sub-field to that sub-field's own `value` from `itemFields`, or `''` when unset.

## Item Limits

- **`minItems`** (default `0`) — the remove button is hidden whenever removing would drop the list below this count. It does **not** pre-create items and is **not validated**; to actually start with items, set the field's `value`.
- **`maxItems`** (default: unlimited) — the add button is hidden once the list reaches this count.

## Props

| Prop          | Type                         | Default      | Description                                              |
| ------------- | ---------------------------- | ------------ | -------------------------------------------------------- |
| `type`        | `'repeater'`                 | -            | Field type (required)                                    |
| `itemFields`  | `Record<string, FieldConfig>` | -            | Sub-field definitions for each item (required)           |
| `minItems`    | `number`                     | `0`          | Hide the remove button at or below this item count       |
| `maxItems`    | `number`                     | - (unlimited) | Hide the add button at this item count                   |
| `addLabel`    | `string`                     | `'Add item'` | Label of the add button                                  |
| `removeLabel` | `string`                     | `'Remove'`   | Label of the remove button                               |
| `label`       | `string`                     | -            | Label shown above the list                               |
| `hideLabel`   | `boolean`                    | `false`      | Hide the label visually                                  |
| `helperText`  | `string`                     | -            | Help text shown below the list                           |
| `value`       | `Array<Record<string, unknown>>` | `[]`     | Initial items                                            |
| `hidden`      | `boolean \| function \| ConditionGroup \| Responsive` | `false` | Hide the field          |
| `columns`     | `Partial<Record<Breakpoint, number>>` | -   | Grid columns by breakpoint                               |

## Validation

The repeater **bypasses schema validation entirely** — the form engine always validates its value as `z.any()`. This means:

- A `schema` (or `required`) set on the repeater field itself is ignored.
- `schema` and `required` settings on the sub-fields inside `itemFields` are **not enforced** either — the form-level validation schema only covers top-level fields, and the whole array is accepted as-is.

If you need to enforce constraints (e.g. at least one item, valid emails inside items), check the array in your `onSubmit` handler or validate server-side.

## How It Works

1. The repeater mounts a `useFieldArray` on the field's name — it relies on the `react-hook-form` context that `<Form>` creates, so it only works inside a rendered form (it can't be used standalone).
2. Each item renders its sub-fields recursively through the same field renderer used for top-level fields, registered under `{name}.{index}.{subName}`.
3. The add button appends a default item built from the `itemFields` definitions; the remove button deletes the item at its index.
4. Add/remove buttons hide themselves according to `maxItems`/`minItems`.

## Required Components

The repeater itself needs `Button` (for the add/remove buttons) plus the field wrapper components (`FormField`, `Field`, `FieldDescription`, `FieldError`). Each sub-field additionally requires its own components — e.g. `Input` for text sub-fields, the `Select` cluster for select sub-fields. See the [Components guide](/docs/components) for how injection works.

## Related

- [Components & dependency injection](/docs/components) — How UI components are provided to fields
- [Validation guide](/docs/validation) — Validation for regular fields (the repeater is exempt, see above)
- [Submit & Actions](/docs/submit) — Where the submitted array ends up
