Skip to content
Brok UI

Loading…

No results

Form Wizard

Open source

A server-ready onboarding wizard with per-step browser validation, restored values, resumable step input, submit progress, server feedback, and unsaved-change protection.

Responsive Dark mode RTL Localized SEO

Preview

resources/views/blocks/form-wizard.blade.php Blade
{{--
    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

terminal
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

form field input select textarea button alert stepper

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.

form-wizard.md
# 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
progress indicator account step profile step review step navigation actions server feedback
Theming hooks
Uses customer surface semantic tokens.

Usage

Render the block as a component, passing data where useful:

resources/views/welcome.blade.php Blade
<x-blocks.form-wizard />

Built from primitives

Composes <brok:*> Semantic tokens Re-themeable RTL-ready

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.

resources/views/blocks/form-wizard.blade.php Blade
{{--
    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