Blocks
The Blocks field lets users build a list of mixed content blocks. Unlike Repeater, where every item uses the same schema, each block stores a type and renders the schema for that type.
Submitted values use this shape:
[
{
"type": "hero",
"data": {
"headline": "Launch faster",
"summary": "Build forms from PHP and render them in your frontend."
}
}
]Basic Usage
use InertiaUI\Forms\Fields\Blocks;
use InertiaUI\Forms\Fields\BlockSet;
use InertiaUI\Forms\Fields\Textarea;
use InertiaUI\Forms\Fields\TextInput;
Blocks::make('content')
->sets([
BlockSet::make('hero')
->label('Hero')
->schema([
TextInput::make('headline')->required(),
Textarea::make('summary')->rows(3),
]),
BlockSet::make('quote')
->label('Quote')
->schema([
Textarea::make('quote')->required(),
TextInput::make('author'),
]),
])
->default([
[
'type' => 'hero',
'data' => [
'headline' => 'Launch faster',
'summary' => 'Build forms from PHP and render them in your frontend.',
],
],
]);The array validation rule is added automatically.
Block Sets
Each BlockSet defines one available block type:
BlockSet::make('callout')
->label('Callout')
->description('Short notice with a tone')
->icon('C')
->schema([
TextInput::make('title')->required(),
TextInput::make('tone')->default('info'),
]);The name passed to BlockSet::make() is trimmed and serialized as the block type, so keep it stable once users have saved data. Every set in one Blocks field must have a unique, non-empty type. Without label(), the type is converted to title case. description() adds chooser text and is also the header description fallback. Without icon(), the chooser shows the first character of the label.
BlockSet is PHP configuration rather than a separate frontend component. Vue and React receive each serialized set through Blocks.
You may call set() instead of sets() when a form assembles block types one at a time:
Blocks::make('content')
->set(BlockSet::make('hero')->schema([...]))
->set(BlockSet::make('quote')->schema([...]));Initial Values and Block Defaults
Use the inherited Blocks::default() method for the initial complete block array when no model value exists. Each item must use the submitted { type, data } shape shown above.
Schema field defaults are used when a block is added. You may also provide set-level defaults:
BlockSet::make('hero')
->default([
'eyebrow' => 'Featured',
])
->schema([
TextInput::make('eyebrow'),
TextInput::make('headline')->required(),
]);Schema field defaults are collected first, then BlockSet::default() overrides matching keys. A known block loaded from a model, Blocks::default(), value, or defaultValue lets its supplied data override those set defaults, and any missing keys are backfilled. Unknown types retain their submitted data so the UI may report them, but Laravel's structural type rule rejects them.
Header Preview
Use preview() to show live data in the collapsed block header:
BlockSet::make('hero')
->preview('headline', 'eyebrow')
->schema([
TextInput::make('eyebrow'),
TextInput::make('headline')->required(),
]);The first argument is the title field. The second argument is the optional description field.
Use previewTitle() or previewDescription() when only one side of the header preview should be configured:
BlockSet::make('hero')
->previewTitle('headline')
->previewDescription('eyebrow');Preview strings are reduced to plain text for the header; numeric 0 and boolean false are preserved. A missing or empty preview title falls back to the set label plus the one-based block position. A missing description preview falls back to the set description.
Launch checklistHeroFeatured
Featured
Limits
Limit the total number of blocks:
Blocks::make('content')
->minItems(1)
->maxItems(3)
->sets([...]);Limit a specific block type:
BlockSet::make('hero')
->maxItems(1);A set at its max remains visible in the chooser but is disabled. On submit, Blocks::maxItems() adds a normal Laravel max: rule to the top-level content array. BlockSet::maxItems() adds custom per-type validation that counts submitted blocks by type and fails when that set exceeds its own max.
Limits must be zero or greater, and the field minimum must not exceed its maximum. A maximum of 0 disables additions for the field or set. Passing null clears a configured limit.
Hero 1Hero
Quote 2Quote
Nested Validation
Blocks validate their structure and then validate nested schema fields for the submitted block type at each request index:
Blocks::make('content')
->maxItems(10)
->sets([
BlockSet::make('hero')
->maxItems(1)
->schema([
TextInput::make('headline')->required(),
]),
BlockSet::make('quote')->schema([
Textarea::make('quote')->required(),
]),
]);A request with a hero at index 0 and a quote at index 1 validates:
[
'content' => ['array', 'max:10', /* per-set max validation */],
'content.*' => ['array'],
'content.*.type' => ['required', 'string', Rule::in(['hero', 'quote'])],
'content.*.data' => ['required', 'array'],
'content.0.data.headline' => ['required'],
'content.1.data.quote' => ['required'],
]Unknown block types fail the structural type rule. Each submitted item must be an array with a data array, and content.N.data.* rules are resolved from the set matching content.N.type. A block set with maxItems() also validates that per-type limit against the top-level content value. See Blocks nested validation for the validation-first reference.
Conditional visibility inside a block is evaluated with that block's data as local data and the complete form as root data. Hidden nested fields receive exclude rules, and unauthorized schema fields are omitted from each serialized set.
Controls
Blocks::make('content')
->sets([...])
->addButtonText('Add Section')
->reorderable()
->collapsible()
->collapsed();Drag-to-reorder, adding, deleting, and collapsing are enabled by default. Use reorderable(false), addable(false), deletable(false), or collapsible(false) to disable them.
Without addButtonText(), the component uses its translated add-block label. Calling collapsed() also enables collapsing.
Launch fasterHeroStart with the main message.
Start with the main message.
The final review was straightforward.QuoteOperations team
Operations team
All shared field methods remain available. See Form Class, Model Binding, Validation, Conditional Visibility, Authorization, and Styling.
Using the Component Directly
Blocks is exported from each stack's components subpath. Pass serialized sets and bind the block array directly. Vue uses v-model; React uses controlled value and onValueChange.
<script setup lang="ts">
import type { BlockItemValue, BlocksField } from '@inertiaui/form-vue'
import { Blocks } from '@inertiaui/form-vue/components'
import { ref } from 'vue'
const sets: BlocksField['sets'] = [
{
type: 'hero',
label: 'Hero',
description: 'Top section for the page',
icon: 'H',
maxItems: 1,
preview: { title: 'headline', description: 'eyebrow' },
defaultData: { eyebrow: 'Featured' },
schema: [
{
component: 'TextInput',
name: 'eyebrow',
label: 'Eyebrow',
inputType: 'text',
required: false,
precognitive: false,
disabled: false,
readonly: false,
autofocus: false,
clearable: false,
copyable: false,
viewable: false,
},
{
component: 'TextInput',
name: 'headline',
label: 'Headline',
inputType: 'text',
required: true,
precognitive: false,
disabled: false,
readonly: false,
autofocus: false,
clearable: false,
copyable: false,
viewable: false,
},
],
},
]
const content = ref<BlockItemValue[] | null>([
{ type: 'hero', data: { eyebrow: 'Featured', headline: 'Launch faster' } },
])
const error = ref<string | null>(null)
const partClasses = {
item: 'ring-zinc-200',
addButton: 'border-zinc-300',
dropdown: 'max-w-md',
}
</script>
<template>
<Blocks
v-model="content"
id="page-content"
name="content"
label="Content"
help="Build the public page sections."
:error="error"
:invalid="!!error"
required
:precognitive="false"
:disabled="false"
:sets="sets"
:min-items="1"
:max-items="5"
:reorderable="true"
:addable="true"
:deletable="true"
:collapsible="true"
:collapsed="false"
add-button-text="Add section"
badge="Page"
tooltip="Each section keeps its own field schema."
help-position="below"
layout="stacked"
control-position="start"
label-class="font-medium"
help-class="text-zinc-500"
error-class="text-red-600"
wrapper-class="max-w-3xl"
control-class="bg-white"
class="space-y-3"
:part-classes="partClasses"
:label-sr-only="false"
/>
</template>import { useState } from 'react'
import type { BlockItemValue, BlocksField } from '@inertiaui/form-react'
import { Blocks } from '@inertiaui/form-react/components'
const sets: BlocksField['sets'] = [
{
type: 'hero',
label: 'Hero',
description: 'Top section for the page',
icon: 'H',
maxItems: 1,
preview: { title: 'headline', description: 'eyebrow' },
defaultData: { eyebrow: 'Featured' },
schema: [
{
component: 'TextInput',
name: 'eyebrow',
label: 'Eyebrow',
inputType: 'text',
required: false,
precognitive: false,
disabled: false,
readonly: false,
autofocus: false,
clearable: false,
copyable: false,
viewable: false,
},
{
component: 'TextInput',
name: 'headline',
label: 'Headline',
inputType: 'text',
required: true,
precognitive: false,
disabled: false,
readonly: false,
autofocus: false,
clearable: false,
copyable: false,
viewable: false,
},
],
},
]
const partClasses = {
item: 'ring-zinc-200',
addButton: 'border-zinc-300',
dropdown: 'max-w-md',
}
export default function ContentBuilder() {
const [content, setContent] = useState<BlockItemValue[] | null>([
{ type: 'hero', data: { eyebrow: 'Featured', headline: 'Launch faster' } },
])
const [error] = useState<string | null>(null)
return (
<Blocks
value={content}
onValueChange={setContent}
id="page-content"
name="content"
label="Content"
help="Build the public page sections."
error={error}
invalid={Boolean(error)}
required
precognitive={false}
disabled={false}
sets={sets}
minItems={1}
maxItems={5}
reorderable
addable
deletable
collapsible
collapsed={false}
addButtonText="Add section"
badge="Page"
tooltip="Each section keeps its own field schema."
helpPosition="below"
layout="stacked"
controlPosition="start"
labelClass="font-medium"
helpClass="text-zinc-500"
errorClass="text-red-600"
wrapperClass="max-w-3xl"
controlClass="bg-white"
class="space-y-3"
partClasses={partClasses}
labelSrOnly={false}
/>
)
}Blocks is a composite control and does not forward arbitrary native attributes or listeners to one inner element. See Native Attributes And Events.
Accessibility
The add button exposes its chooser with aria-expanded and aria-controls; Escape and outside interaction close it. Chooser options are buttons and unavailable sets remain visible but disabled. Every block has keyboard move buttons in addition to pointer dragging, with live reorder announcements and focus following a move. Collapsible blocks use native details and summary semantics. Deleting opens a labeled confirmation dialog, keeps focus inside it, and restores focus when it closes.