Form Wizard
A server-ready onboarding wizard with per-step browser validation, restored values, resumable step input, submit progress, server feedback, and unsaved-change protection.
Preview
{{--
Form Wizard 01 — a multi-step form with a reactive progress indicator.
State (current step + nav) is inline Alpine on a plain <div> (never on an
<x-ui.card>: bracketed x-data on a component tag breaks Blade parsing). Each
step is a panel toggled by `x-show`; Back is hidden on step 1 and the submit
button replaces Continue on the last step. Composes the form primitives.
--}}
@props([
'action' => '#',
'method' => 'POST',
'currentStep' => 1,
'values' => null,
'statusMessage' => null,
'submitLabel' => 'Create account',
])
@php
$steps = [__('Account'), __('Profile'), __('Review')];
$currentStep = max(1, min(count($steps), (int) $currentStep));
$values ??= ['email' => '', 'full_name' => '', 'role' => 'developer', 'bio' => ''];
@endphp
<x-ui.form.guard>
<div
data-slot="block-form-wizard"
x-data="{
step: @js($currentStep),
dir: 'fwd',
steps: @js($steps),
get total() { return this.steps.length },
next() {
const panel = this.$root.querySelector(`[data-step-panel='${this.step}']`)
const invalid = Array.from(panel?.querySelectorAll('input, select, textarea') ?? []).find((control) => ! control.checkValidity())
if (invalid) { invalid.reportValidity(); invalid.focus(); return }
if (this.step < this.total) { this.dir = 'fwd'; this.step++ }
},
back() { if (this.step > 1) { this.dir = 'back'; this.step-- } },
}"
:data-step-dir="dir"
class="mx-auto w-full max-w-xl rounded-lg border border-border bg-card p-6 text-card-foreground shadow-sm sm:p-8"
>
{{-- Reactive progress indicator: the stepper follows `step` live. --}}
<x-ui.stepper
orientation="horizontal"
bind="step"
:steps="collect($steps)->map(fn (string $label, int $i) => ['title' => $label, 'status' => $currentStep > $i + 1 ? 'completed' : ($currentStep === $i + 1 ? 'active' : 'default')])->all()"
class="mb-8"
aria-label="{{ __('Progress') }}"
/>
@if ($statusMessage)
<x-ui.alert variant="success" role="status" class="mb-6">{{ __($statusMessage) }}</x-ui.alert>
@endif
<x-ui.form :action="$action" :method="$method" :summary="__('Please correct the errors below.')" data-persisted-form>
{{-- Step 1 — Account --}}
<div x-show="step === 1" data-step-panel="1" class="space-y-4">
<x-ui.field name="email" label="Email">
<x-ui.input name="email" type="email" :value="$values['email'] ?? ''" placeholder="you@example.com" required />
</x-ui.field>
<x-ui.field name="password" label="Password">
<x-ui.input name="password" type="password" placeholder="••••••••" autocomplete="new-password" minlength="8" required />
</x-ui.field>
</div>
{{-- Step 2 — Profile --}}
<div x-show="step === 2" x-cloak data-step-panel="2" class="space-y-4">
<x-ui.field name="full_name" label="Full name">
<x-ui.input name="full_name" :value="$values['full_name'] ?? ''" placeholder="Ada Lovelace" required />
</x-ui.field>
<x-ui.field name="role" label="Role">
<x-ui.select name="role" required>
<option value="developer" @selected(old('role', $values['role'] ?? '') === 'developer')>{{ __('Developer') }}</option>
<option value="designer" @selected(old('role', $values['role'] ?? '') === 'designer')>{{ __('Designer') }}</option>
<option value="manager" @selected(old('role', $values['role'] ?? '') === 'manager')>{{ __('Manager') }}</option>
</x-ui.select>
</x-ui.field>
<x-ui.field name="bio" label="Bio">
<x-ui.textarea name="bio" rows="3" placeholder="A sentence about you.">{{ $values['bio'] ?? '' }}</x-ui.textarea>
</x-ui.field>
</div>
{{-- Step 3 — Review --}}
<div x-show="step === 3" x-cloak data-step-panel="3" class="space-y-4">
<p class="text-sm text-muted-foreground">{{ __('Review your details, then create your account.') }}</p>
<div class="rounded-lg border border-border bg-muted/40 p-6 text-sm text-foreground">
{{ __('Everything look right? Use Back to make changes.') }}
</div>
</div>
{{-- Navigation --}}
<div class="mt-8 flex items-center gap-4">
<x-ui.button type="button" variant="outline" x-show="step > 1" x-on:click="back()">
{{ __('Back') }}
</x-ui.button>
<div class="ms-auto flex items-center gap-4">
<span class="text-xs text-muted-foreground" aria-hidden="true">
<span x-text="step"></span> / <span x-text="total"></span>
</span>
<x-ui.button type="button" x-show="step < total" x-on:click="next()">
{{ __('Continue') }}
</x-ui.button>
<x-ui.button type="submit" x-show="step === total" x-cloak x-bind:disabled="submitting" x-bind:aria-busy="submitting">
<span x-show="! submitting">{{ __($submitLabel) }}</span>
<span x-cloak x-show="submitting">{{ __('Creating…') }}</span>
</x-ui.button>
</div>
</div>
</x-ui.form>
</div>
</x-ui.form.guard>
Installation
php artisan ui:add blocks/form-wizard
Registry contract
Install confidence
php artisan ui:add blocks/form-wizard
writes only the generated targets below. The CLI validates each file hash before writing and
prompts before replacing local changes unless --force is used.
- Version
- 1.4.1
- License
- open
- Stability
- stable
- Contract
- v2
- Foundation
- ≥ 1.0.0
| Type | Generated target |
|---|---|
| blade | resources/views/blocks/form-wizard.blade.php |
Registry dependencies
Package dependencies
composer: jml/brok:^0.2
npm: alpinejs
Use with AI
A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.
# Brok UI block: Form Wizard (`form-wizard`)
A server-ready onboarding wizard with per-step browser validation, restored values, resumable step input, submit progress, server feedback, and unsaved-change protection.
Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.
## Install
```bash
php artisan ui:add blocks/form-wizard
```
## Render it
```blade
<x-blocks.form-wizard />
```
## Props
- `action` (string, default `#`) — Final onboarding endpoint.
- `method` (string, default `POST`) — Final request method.
- `current-step` (int, default `1`) — Initial server-owned step, clamped from one to three.
- `values` (array|null, default `null`) — Restored non-secret field values.
- `status-message` (string|null, default `null`) — Success feedback after redirect.
- `submit-label` (string, default `Create account`) — Final submit action label.
## Use when
- A long form has meaningful stages and users benefit from progress context.
## Avoid when
- The form is short enough to scan and complete on one screen.
## Rules
- Render the installed block with `<x-blocks.form-wizard />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.
## Links
- Docs: https://brokui.dev/blocks/form-wizard
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/form-wizard.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Guidance
Use when
- A long form has meaningful stages and users benefit from progress context.
Avoid when
- The form is short enough to scan and complete on one screen.
Anti-patterns
- Do not replace semantic props with conflicting utility classes.
- Do not remove labels, focus styles, or state attributes.
- Anatomy
- Theming hooks
Usage
Render the block as a component, passing data where useful:
<x-blocks.form-wizard />
Built from primitives
This block composes installed <brok:*> primitives and semantic design
tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by
editing CSS variables; flip the preview to RTL to confirm it mirrors.
Source
The exact, editable file ui:add writes
into your app. The preview above renders this same source — there are no preview-only blocks.
{{--
Form Wizard 01 — a multi-step form with a reactive progress indicator.
State (current step + nav) is inline Alpine on a plain <div> (never on an
<x-ui.card>: bracketed x-data on a component tag breaks Blade parsing). Each
step is a panel toggled by `x-show`; Back is hidden on step 1 and the submit
button replaces Continue on the last step. Composes the form primitives.
--}}
@props([
'action' => '#',
'method' => 'POST',
'currentStep' => 1,
'values' => null,
'statusMessage' => null,
'submitLabel' => 'Create account',
])
@php
$steps = [__('Account'), __('Profile'), __('Review')];
$currentStep = max(1, min(count($steps), (int) $currentStep));
$values ??= ['email' => '', 'full_name' => '', 'role' => 'developer', 'bio' => ''];
@endphp
<x-ui.form.guard>
<div
data-slot="block-form-wizard"
x-data="{
step: @js($currentStep),
dir: 'fwd',
steps: @js($steps),
get total() { return this.steps.length },
next() {
const panel = this.$root.querySelector(`[data-step-panel='${this.step}']`)
const invalid = Array.from(panel?.querySelectorAll('input, select, textarea') ?? []).find((control) => ! control.checkValidity())
if (invalid) { invalid.reportValidity(); invalid.focus(); return }
if (this.step < this.total) { this.dir = 'fwd'; this.step++ }
},
back() { if (this.step > 1) { this.dir = 'back'; this.step-- } },
}"
:data-step-dir="dir"
class="mx-auto w-full max-w-xl rounded-lg border border-border bg-card p-6 text-card-foreground shadow-sm sm:p-8"
>
{{-- Reactive progress indicator: the stepper follows `step` live. --}}
<x-ui.stepper
orientation="horizontal"
bind="step"
:steps="collect($steps)->map(fn (string $label, int $i) => ['title' => $label, 'status' => $currentStep > $i + 1 ? 'completed' : ($currentStep === $i + 1 ? 'active' : 'default')])->all()"
class="mb-8"
aria-label="{{ __('Progress') }}"
/>
@if ($statusMessage)
<x-ui.alert variant="success" role="status" class="mb-6">{{ __($statusMessage) }}</x-ui.alert>
@endif
<x-ui.form :action="$action" :method="$method" :summary="__('Please correct the errors below.')" data-persisted-form>
{{-- Step 1 — Account --}}
<div x-show="step === 1" data-step-panel="1" class="space-y-4">
<x-ui.field name="email" label="Email">
<x-ui.input name="email" type="email" :value="$values['email'] ?? ''" placeholder="you@example.com" required />
</x-ui.field>
<x-ui.field name="password" label="Password">
<x-ui.input name="password" type="password" placeholder="••••••••" autocomplete="new-password" minlength="8" required />
</x-ui.field>
</div>
{{-- Step 2 — Profile --}}
<div x-show="step === 2" x-cloak data-step-panel="2" class="space-y-4">
<x-ui.field name="full_name" label="Full name">
<x-ui.input name="full_name" :value="$values['full_name'] ?? ''" placeholder="Ada Lovelace" required />
</x-ui.field>
<x-ui.field name="role" label="Role">
<x-ui.select name="role" required>
<option value="developer" @selected(old('role', $values['role'] ?? '') === 'developer')>{{ __('Developer') }}</option>
<option value="designer" @selected(old('role', $values['role'] ?? '') === 'designer')>{{ __('Designer') }}</option>
<option value="manager" @selected(old('role', $values['role'] ?? '') === 'manager')>{{ __('Manager') }}</option>
</x-ui.select>
</x-ui.field>
<x-ui.field name="bio" label="Bio">
<x-ui.textarea name="bio" rows="3" placeholder="A sentence about you.">{{ $values['bio'] ?? '' }}</x-ui.textarea>
</x-ui.field>
</div>
{{-- Step 3 — Review --}}
<div x-show="step === 3" x-cloak data-step-panel="3" class="space-y-4">
<p class="text-sm text-muted-foreground">{{ __('Review your details, then create your account.') }}</p>
<div class="rounded-lg border border-border bg-muted/40 p-6 text-sm text-foreground">
{{ __('Everything look right? Use Back to make changes.') }}
</div>
</div>
{{-- Navigation --}}
<div class="mt-8 flex items-center gap-4">
<x-ui.button type="button" variant="outline" x-show="step > 1" x-on:click="back()">
{{ __('Back') }}
</x-ui.button>
<div class="ms-auto flex items-center gap-4">
<span class="text-xs text-muted-foreground" aria-hidden="true">
<span x-text="step"></span> / <span x-text="total"></span>
</span>
<x-ui.button type="button" x-show="step < total" x-on:click="next()">
{{ __('Continue') }}
</x-ui.button>
<x-ui.button type="submit" x-show="step === total" x-cloak x-bind:disabled="submitting" x-bind:aria-busy="submitting">
<span x-show="! submitting">{{ __($submitLabel) }}</span>
<span x-cloak x-show="submitting">{{ __('Creating…') }}</span>
</x-ui.button>
</div>
</div>
</x-ui.form>
</div>
</x-ui.form.guard>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.4.1 - Status
- Stable
- License
-
open - Deprecation
- Not deprecated
- Contract
-
v2 - Foundation
-
≥ 1.0.0