Wizard
A wizard shows a form one part at a time. Every fieldset becomes a step, a stepper at the top shows where the user is, and Continue checks the current step on the server before moving on. It is still one form class, one data object and one final submit.
When to use: sign-up and onboarding flows, checkout, applications and long intake forms, where a single page of fields would feel like too much.
use Erag\InertiaForms\Fields\Fieldset;
use Erag\InertiaForms\Fields\OtpInput;
use Erag\InertiaForms\Fields\Slug;
use Erag\InertiaForms\Fields\Submit;
use Erag\InertiaForms\Fields\TextInput;
use Erag\InertiaForms\Form;
class SignupForm extends Form
{
protected ?string $actionRoute = 'signup.store';
protected bool $wizard = true;
public function fields(): array
{
return [
Fieldset::make('Account')->description('Your login details')->icon('user')->fields([
TextInput::make('name')->required(),
TextInput::make('email')->email()->required(),
TextInput::make('password')->password()->minLength(8)->required(),
]),
Fieldset::make('Workspace')->description('Name your team space')->icon('briefcase')->fields([
TextInput::make('workspace')->required(),
Slug::make('workspace_url')->from('workspace')->prefix('app.example.test/')->required(),
]),
Fieldset::make('Verify')->description('Enter the code we emailed you')->icon('shield')->fields([
OtpInput::make('code')->length(6)->required(),
]),
Submit::make('Create account')->processingLabel('Creating…'),
];
}
}The controller and the page don't change: pass SignupForm::make() to Inertia, render <Form>, and validate with #[Validate] as usual.
Try it on the Onboarding wizard form in the Live Demo.
Examples
Each example is a live wizard built from the PHP below it. The docs have no server, so Continue skips the step check here and only the final submit checks required fields.
Basic
Two fieldsets, two steps. The trailing Submit appears on the last step.
use Erag\InertiaForms\Fields\Fieldset;
use Erag\InertiaForms\Fields\Submit;
use Erag\InertiaForms\Fields\Textarea;
use Erag\InertiaForms\Fields\TextInput;
use Erag\InertiaForms\Form;
class SupportRequestForm extends Form
{
protected bool $wizard = true;
public function fields(): array
{
return [
Fieldset::make('About you')->fields([
TextInput::make('name')->required(),
TextInput::make('email')->email()->required(),
]),
Fieldset::make('Your question')->fields([
TextInput::make('subject')->required(),
Textarea::make('message')->rows(4)->required(),
]),
Submit::make('Send request'),
];
}
}Icons, descriptions and labels
Three steps with an icon and a description each, plus custom button labels set as properties.
use Erag\InertiaForms\Fields\Combobox;
use Erag\InertiaForms\Fields\Fieldset;
use Erag\InertiaForms\Fields\OtpInput;
use Erag\InertiaForms\Fields\Slug;
use Erag\InertiaForms\Fields\Submit;
use Erag\InertiaForms\Fields\TextInput;
use Erag\InertiaForms\Form;
class SignupForm extends Form
{
protected bool $wizard = true;
protected string $wizardNextLabel = 'Next';
protected string $wizardBackLabel = 'Previous';
public function fields(): array
{
return [
Fieldset::make('Account')->description('Your login details')->icon('user')->fields([
TextInput::make('name')->required(),
TextInput::make('email')->email()->required(),
TextInput::make('password')->password()->minLength(8)->required(),
]),
Fieldset::make('Workspace')->description('Name your team space')->icon('briefcase')->columns(2)->fields([
TextInput::make('workspace')->required(),
Combobox::make('team_size')->options(['1-10', '11-50', '51-200', '200+']),
Slug::make('workspace_url')->from('workspace')->prefix('app.example.test/')->required()->columnSpan(2),
]),
Fieldset::make('Verify')->description('Enter the code we emailed you')->icon('shield')->fields([
OtpInput::make('code')->length(6)->required(),
]),
Submit::make('Create account')->processingLabel('Creating…'),
];
}
}Advanced: conditional steps
A checkout where the second step depends on the first answer: Shipping for delivery, Pickup for collection. Switch the delivery option and the stepper changes. The wizard is turned on in the constructor, which keeps it working with the server step check.
use Erag\InertiaForms\Fields\Checkbox;
use Erag\InertiaForms\Fields\DatePicker;
use Erag\InertiaForms\Fields\Fieldset;
use Erag\InertiaForms\Fields\Radio;
use Erag\InertiaForms\Fields\Submit;
use Erag\InertiaForms\Fields\TextInput;
use Erag\InertiaForms\Form;
class CheckoutForm extends Form
{
public function __construct()
{
$this->wizard(nextLabel: 'Next step', backLabel: 'Go back');
}
public function fields(): array
{
return [
Fieldset::make('Order')->description('How do you want it?')->icon('fileText')->fields([
TextInput::make('quantity')->number()->min(1)->max(10)->default(1)->required(),
Radio::make('delivery')->buttons()->default('ship')->required()->options([
'ship' => 'Ship to me',
'pickup' => 'Pick up in store',
]),
]),
Fieldset::make('Shipping')
->description('Only when shipping')
->icon('mapPin')
->visibleWhen('delivery', 'ship')
->columns(2)
->fields([
TextInput::make('shipping.street')->required()->columnSpan(2),
TextInput::make('shipping.city')->required(),
TextInput::make('shipping.postal_code')->required(),
]),
Fieldset::make('Pickup')
->description('Only when picking up')
->icon('home')
->visibleWhen('delivery', 'pickup')
->fields([
DatePicker::make('pickup_date')->required(),
]),
Fieldset::make('Payment')->description('Card details')->icon('creditCard')->columns(2)->fields([
TextInput::make('card.number')->label('Card number')->required()->columnSpan(2),
TextInput::make('card.expiry')->label('Expiry (MM/YY)')->required(),
TextInput::make('card.cvc')->label('CVC')->required(),
Checkbox::make('terms')->label('I accept the terms of sale')->required()->columnSpan(2),
]),
Submit::make('Place order')->processingLabel('Placing order…'),
];
}
}Turning it on
Set the $wizard property in the class, as above. The button texts can be set the same way:
protected bool $wizard = true;
protected string $wizardNextLabel = 'Next';
protected string $wizardBackLabel = 'Previous';Or call wizard() with named arguments:
public function __construct()
{
$this->wizard(nextLabel: 'Next step', backLabel: 'Go back');
}wizard(bool $wizard = true, ?string $nextLabel = null, ?string $backLabel = null) defaults to Continue and Back. wizard(false) turns it off again.
Turn it on inside the class
Step checks run on a package endpoint that rebuilds your form with YourForm::make(), without arguments, the same way remote combobox search does. The endpoint only answers for forms that are a wizard after make(), so set $wizard (or call wizard() in the constructor) rather than calling ->wizard() in a controller. For the same reason, the fields of a wizard must not depend on a bound model or on constructor arguments to decide their rules.
Steps
- Each visible fieldset that holds at least one field other than a Submit button is a step. Loose fields between fieldsets are grouped into a step of their own, like on a normal form.
- The stepper shows each step's
icon(),legend()anddescription(). - A fieldset with
visibleWhen()is a step only while it is visible, so steps can appear and disappear as the user answers earlier questions. - Submit buttons are shown on the last step. A trailing
Submitoutside any fieldset, like in the example, does not make an extra step.
Fieldset::icon(?string $icon)
A package icon name shown for the step in the stepper, for example user. It has no effect outside a wizard. See Icons for every name.
Moving between steps
- Back goes to the previous step without checking anything.
- Continue sends the current step's values to the server. When they pass, the next step opens; when they fail, the messages appear under the fields, exactly like after a normal submit.
- Pressing Enter in a field continues to the next step instead of submitting the form. On the last step it submits.
- File fields (File Upload, Composer attachments) are skipped by the step check and validated with the final submit, since files are only sent then.
- The final submit validates the whole form again. If it fails, the wizard jumps to the step that holds the first invalid field.
How a step is checked
Continue posts to POST /_inertia-forms/validate-step with an encrypted form token, the step index and the current data. The package:
- decrypts the form class and rejects anything that isn't one of your
Formclasses (404), - builds the form with
make()and checks that it is a wizard (404) and that itsauthorize()passes (403), - runs the rules of the visible fields in that step, with your
messages()andattributes(), - answers
204 No Contentwhen the step is valid, or the usual422JSON with errors.
Nothing is saved by a step check. The data is only trusted after the final validate() in your controller.
The route name is inertia-forms.validate-step. Change its path or middleware under wizard in the config file.
You can run the same checks yourself:
$form = SignupForm::make();
$form->wizardSteps($data); // the fieldsets that are steps for this data
$form->validateStep(0, $data); // throws ValidationException when step 0 is invalidSchema
A wizard form serializes a wizard object (it is null for other forms):
"wizard": {
"nextLabel": "Continue",
"backLabel": "Back",
"validateUrl": "https://example.test/_inertia-forms/validate-step",
"token": "eyJpdiI6..."
}See Serialized Schema.