Builder
The builder renders a repeatable list of rows where each row can be a different block type. You define
the available block types up front, and every block carries its own schema of fields. A row records
which block it is through an automatic hidden type discriminator, so the field submits an array of
row objects — each tagged with its type and keyed by that block’s field names. Create one with
Builder::make() and list the blocks with ->blocks().
Builder::make('items', 'Line items') ->blocks([ Block::make('text')->label('Text')->schema([ Textarea::make('content', 'Content')->required(), ]), Block::make('product')->label('Product line')->schema([ TextInput::make('product', 'Product')->required(), TextInput::make('qty', 'Qty')->rules(['numeric']), TextInput::make('price', 'Price')->rules(['numeric']), ]), ]) ->minItems(1) ->addLabel('Add block')- addLabel:"Add block"
- columnWidth:"md"
- conditions:null
- defaultItems:0
- dependsOnAny:false
- dependsOnKeys:null
- disabled:false
- editablePrefill:false
- helperText:null
- hidden:false
- label:"Line items"
- layout:"stack"
- maxItems:null
- minItems:1
- name:"items"
- prefillRefreshOn:null
- prefillResetOn:null
- readOnly:false
- reorderable:true
- required:false
- resizableColumns:false
- resizeIndicator:false
- rowActions:null
- tooltip:null
- value:null
Blocks
Section titled “Blocks”->blocks() takes the list of block types the builder offers. Each is a Block::make($type) where
$type is the discriminator stored on every row of that kind. Give it a human label with ->label()
and its row template with ->schema(); the schema is a normal array of fields, so each child keeps its
own label, placeholder, default value, and rules.
Block::make('product') ->label('Product line') ->schema([ TextInput::make('product', 'Product')->required(), TextInput::make('qty', 'Qty')->rules(['numeric']), ]);The example above submits as an array of typed rows:
['items' => [ ['type' => 'text', 'content' => 'Thanks for your order.'], ['type' => 'product', 'product' => 'Widget', 'qty' => '3', 'price' => '9.99'],]]Adding rows
Section titled “Adding rows”Each builder row is inserted through an add menu listing every block by its label. Picking a block
appends an empty row of that type. ->addLabel() sets the text on the button that opens the menu
(default “Add”).
Builder::make('items', 'Line items') ->blocks([Block::make('text')->schema([Textarea::make('content')])]) ->addLabel('Add block');Row counts
Section titled “Row counts”->minItems() and ->maxItems() bound how many rows the form accepts; both are enforced as
array-level validation rules independent of which block each row is.
Builder::make('items', 'Line items') ->blocks([Block::make('text')->schema([Textarea::make('content')])]) ->minItems(1) ->maxItems(20);Reordering
Section titled “Reordering”Rows are reorderable by default through up/down controls. Pass ->reorderable(false) to fix the
order.
Builder::make('items', 'Line items') ->blocks([Block::make('text')->schema([Textarea::make('content')])]) ->reorderable(false);Table layout
Section titled “Table layout”Rows stack by default — each row is a full block of stacked fields. Call ->table() to lay rows out
as a table instead. The columns come from the first block’s schema, a shared header of those field
labels sits above the rows, and each cell renders the input alone (the field’s own label moves up into
the header). A row of any other block type spans the full width below the header rather than fitting
the columns.
Builder::make('items', 'Line items') ->blocks([ Block::make('product')->label('Product line')->schema([ TextInput::make('product', 'Product'), TextInput::make('qty', 'Qty')->rules(['numeric']), ]), Block::make('text')->label('Text')->schema([ Textarea::make('content', 'Content'), ]), ]) ->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.
Row actions
Section titled “Row actions”Like the repeater, the builder takes ->rowActions([RowAction::duplicate(), RowAction::remove()]) to
declare the per-row menu — the built-in Duplicate and Remove actions, customisable with
->label()/->icon()/->destructive(). See Repeater → Row actions
for the full rundown.
Validation
Section titled “Validation”Each row is validated against the schema of its own block: a child marked ->required() is required
only in rows of that block type, and an error points at the offending row. The hidden type
discriminator is validated too — a row whose type is not one of the declared blocks is rejected. 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.
Not yet supported (v1)
Section titled “Not yet supported (v1)”These are follow-ups, not part of the first release:
- Built-in image and subtotal blocks.
- Nested builders (a builder inside a builder row).
- Customer-aware price prefill — tracked as separate upcoming work.
Common options
Section titled “Common options”Builder shares label, required, disabled, read-only, and visibility options with every field — see
Fields. For validation and conditional behavior, see
Validation and Conditional fields.