Skip to content

Slots and Children

Introduction

The Form component renders fields automatically based on the PHP configuration. Vue exposes named slots for field replacement, field insertion, and fieldset insertion. React exposes children after the generated fieldsets and a structured slots render map for insertion around fields and fieldsets.

Default Slot

Content placed in the Vue default slot or React children is rendered after all fieldsets.

vue
<Form :form="form">
    <template #default="{ form }">
        <p v-if="form.isDirty.value" class="text-sm text-iui-form-warning">
            You have unsaved changes.
        </p>
    </template>
</Form>
tsx
<Form form={form}>
    {(formInstance) => (
        formInstance.isDirty ? (
            <p className="text-iui-form-warning text-sm">
                You have unsaved changes.
            </p>
        ) : null
    )}
</Form>

The slot prop or render-prop argument is the return value of useForm, so you have access to form.data, form.errors, form.processing, form.submit(), and everything documented in Customizing Presenters.

Adding Content Around the Form

Since the default slot renders after fieldsets, you may combine it with wrapper markup to add headers, footers, or surrounding content:

vue
<div>
    <h2 class="text-xl font-bold mb-4">Create User</h2>

    <Form :form="form">
        <template #default="{ form }">
            <div class="flex justify-between items-center mt-6 pt-6 border-t">
                <p class="text-sm text-iui-form-muted-foreground">
                    All fields marked with * are required.
                </p>
            </div>
        </template>
    </Form>
</div>
tsx
<div>
    <h2 className="mb-4 text-xl font-bold">Create User</h2>

    <Form form={form}>
        <div className="mt-6 flex items-center justify-between border-t pt-6">
            <p className="text-iui-form-muted-foreground text-sm">
                All fields marked with * are required.
            </p>
        </div>
    </Form>
</div>

Replacing Field Rendering

In Vue, use a named slot matching the field name plus -field to replace one auto-rendered field while keeping the PHP-defined order:

vue
<Form :form="form">
    <template #email-field="{ field, value, error, setValue }">
        <div>
            <label class="mb-1 block text-sm font-medium">
                {{ field.label }}
            </label>

            <input
                class="block w-full rounded-md"
                :value="value ?? ''"
                @input="setValue($event.target.value)"
            />

            <p v-if="error" class="mt-1 text-sm text-iui-form-destructive">
                {{ error }}
            </p>
        </div>
    </template>
</Form>

The slot is rendered exactly where the field appears in the backend schema, including fields inside fieldsets. Without a matching slot, the default field component renders as usual.

Field replacement slots receive:

PropDescription
fieldThe serialized field configuration from PHP.
formThe useForm return value used by the generated form.
contextThe injected form context used by field components.
nameThe field name.
valueThe current form value for the field.
errorThe validation error for the field, if any.
errorsThe full form error object.
setValueUpdates the form value and clears the field error.
fieldClassLayout class the renderer would have passed to the default field wrapper.
inGridWhether the field is currently rendered in a multi-column fieldset grid.

React does not expose a field replacement renderer. Use the insertion render map below for adjacent content, or use manual form rendering when you need to replace the generated field itself.

Inserting Content Around Fields

In Vue, use #before-{name}-field and #after-{name}-field to add content immediately around one generated field without replacing it. In React, use slots.fields[name].before and slots.fields[name].after. These insertion points render around the default field component or, in Vue, around a matching replacement slot such as #email-field:

vue
<Form :form="form">
    <template #before-email-field="{ field }">
        <p class="text-sm text-iui-form-muted-foreground">
            We will use {{ field.label }} for account notifications.
        </p>
    </template>

    <template #after-email-field="{ value }">
        <p v-if="value" class="text-sm text-iui-form-muted-foreground">
            You can change this address later.
        </p>
    </template>
</Form>
tsx
<Form
    form={form}
    slots={{
        fields: {
            email: {
                before: ({ field }) => (
                    <p className="text-iui-form-muted-foreground text-sm">
                        We will use {field.label} for account notifications.
                    </p>
                ),
                after: ({ value }) =>
                    value ? (
                        <p className="text-iui-form-muted-foreground text-sm">
                            You can change this address later.
                        </p>
                    ) : null,
            },
        },
    }}
/>

Vue field insertion slots and React field renderers receive the field props listed above. The default field still renders between the before and after insertion points unless you also provide the Vue replacement slot for that field.

Vue slot names use the serialized field name plus -field. React field keys use the serialized field name directly. For field names that are not valid static Vue slot names, use dynamic slot names:

vue
<template #[`user.email-field`]="slotProps">
    <!-- ... -->
</template>

<template #[`before-user.email-field`]="slotProps">
    <!-- ... -->
</template>
tsx
<Form
    form={form}
    slots={{
        fields: {
            'user.email': {
                before: ({ name }) => (
                    <p className="text-iui-form-muted-foreground text-sm">
                        Extra detail for {name}.
                    </p>
                ),
            },
        },
    }}
/>

Inserting Content Around Fieldsets

In Vue, use #before-{id}-fieldset and #after-{id}-fieldset to add content immediately around a generated fieldset. In React, use slots.fieldsets[id].before and slots.fieldsets[id].after. Fieldset targeting requires an explicit Fieldset::id(...) value. Fieldsets are not targeted by legend text or array position.

php
use InertiaUI\Forms\Fields\Fieldset;
use InertiaUI\Forms\Fields\TextInput;

Fieldset::make()
    ->id('account')
    ->legend('Account')
    ->fields([
        TextInput::make('email')->label('Email'),
    ]);
vue
<Form :form="form">
    <template #before-account-fieldset="{ fieldset }">
        <p class="text-sm text-iui-form-muted-foreground">
            {{ fieldset.legend }} details
        </p>
    </template>

    <template #after-account-fieldset="{ id }">
        <p class="text-sm text-iui-form-muted-foreground">
            End of {{ id }}.
        </p>
    </template>
</Form>
tsx
<Form
    form={form}
    slots={{
        fieldsets: {
            account: {
                before: ({ fieldset }) => (
                    <p className="text-iui-form-muted-foreground text-sm">
                        {fieldset.legend} details
                    </p>
                ),
                after: ({ id }) => (
                    <p className="text-iui-form-muted-foreground text-sm">
                        End of {id}.
                    </p>
                ),
            },
        },
    }}
/>

Fieldset insertion slots and renderers receive:

PropDescription
fieldsetThe serialized fieldset configuration from PHP.
formThe useForm return value used by the generated form.
contextThe injected form context used by field components.
idThe explicit fieldset id.
fieldClassLayout class when provided by the fieldset renderer.
inGridWhether the fieldset uses a multi-column grid.

For complete control over every field and the form shell itself, use manual form rendering. Manual Usage shows the provider setup and direct component pattern for both Vue and React.

Mixing Auto-Render and Custom Content

You may use the default slot to add custom content alongside auto-rendered fields. The auto-rendered fieldsets appear first, followed by your slot content:

vue
<Form :form="form">
    <template #default="{ form }">
        <!-- This appears after all auto-rendered fields -->
        <div class="bg-iui-form-info-subtle rounded-iui-form-lg p-4 mt-4">
            <h3 class="font-medium text-iui-form-info-foreground">Preview</h3>
            <p>Name: {{ form.data.name }}</p>
            <p>Email: {{ form.data.email }}</p>
        </div>
    </template>
</Form>

Wizard Additional Content

The Vue Wizard component provides a default slot with both form and wizard props. React accepts an equivalent render function as children:

vue
<Wizard :form="form">
    <template #default="{ form, wizard }">
        <p class="text-sm text-iui-form-muted-foreground mt-4">
            Step {{ wizard.currentStep.value + 1 }} of {{ wizard.totalSteps.value }}
        </p>
    </template>
</Wizard>
tsx
<Wizard form={form}>
    {(generatedForm, wizard) => <p>Step {wizard.currentStep + 1} of {wizard.totalSteps}</p>}
</Wizard>

Vue field and fieldset insertion slots and the React slots render map work around the active wizard step content the same way they work in Form.

Using Individual Input Components

Direct input components are exported from @inertiaui/form-vue/components and @inertiaui/form-react/components and may be used in manual form layouts. RichText is exported from each package's richtext subpath so Tiptap stays out of the general component barrel. With form context in the component tree, these components bind to form data automatically. Pass the serialized field config into the component:

vue
<script setup lang="ts">
import { SubmitButton, Textarea, TextInput } from '@inertiaui/form-vue/components'
</script>

<template>
    <TextInput v-bind="form.getField('name')" />
</template>
tsx
import { SubmitButton, Textarea, TextInput } from '@inertiaui/form-react/components'

export function CustomLayout({ form }) {
    return <TextInput {...form.getField('name')} />
}

This is useful when you want the convenience of the pre-built input components but need a custom layout that the auto-render Form component does not support. For components that do not use useForm or serialized form.getField(...) payloads, see Manual Usage.