Skip to main content
Urbicon UI
Back to Recipes

Multi-Step Wizard

Step-by-step form wizard with validation, progress tracking, and review step.

Live Preview

  1. 1
    Account Personal info
  2. 2
    Preferences Your choices
  3. 3
    Review Confirm details

Key Features

  • Stepper with labeled steps and completion states
  • Per-step input validation before advancing
  • Select menu for structured choices
  • RadioGroup for single-choice selections
  • Textarea for free-form input
  • Review step with editable summary
  • Progress bar tracking overall completion

Code

Multi-Step Wizard Recipe

<script lang="ts">
  import {
    Stepper, StepperStep, Input, Select, RadioGroup, RadioItem,
    Textarea, Checkbox, Button, Card, Progress, Alert
  } from '@urbicon-ui/blocks';

  let step = $state(0);
  let submitted = $state(false);

  // Step 0: Account
  let fullName = $state('');
  let email = $state('');

  // Step 1: Preferences
  let plan = $state('');
  let region = $state<string | null>(null);

  // Step 2: Review
  let notes = $state('');
  let agreedToTerms = $state(false);

  const regionOptions = [
    { label: 'United States', value: 'us' },
    { label: 'Europe', value: 'eu' },
    { label: 'Asia Pacific', value: 'asia' }
  ];

  const planLabels: Record<string, string> = {
    starter: 'Starter',
    pro: 'Professional',
    enterprise: 'Enterprise'
  };

  let progress = $derived(submitted ? 100 : Math.round((step / 3) * 100));

  let canNext = $derived.by(() => {
    if (step === 0) return fullName.trim() !== '' && email.trim() !== '';
    if (step === 1) return plan !== '' && region !== null;
    if (step === 2) return agreedToTerms;
    return false;
  });

  function next() {
    if (step < 2) step += 1;
    else if (canNext) submitted = true;
  }

  function back() {
    if (step > 0) step -= 1;
  }

  function reset() {
    step = 0;
    submitted = false;
    fullName = '';
    email = '';
    plan = '';
    region = null;
    notes = '';
    agreedToTerms = false;
  }
</script>

<div class="mx-auto max-w-xl p-8">
  <Progress value={progress} size="sm" intent="primary" class="mb-6" />

  <Stepper bind:activeStep={step} orientation="horizontal">
    <StepperStep label="Account" description="Personal info" />
    <StepperStep label="Preferences" description="Your choices" />
    <StepperStep label="Review" description="Confirm details" />
  </Stepper>

  {#if submitted}
    <div class="mt-8">
      <Alert intent="success" variant="soft" title="All done!">
        Your wizard has been submitted successfully.
      </Alert>
      <Button variant="outlined" intent="neutral" onclick={reset} class="mt-4">
        Start Over
      </Button>
    </div>
  {:else}
    <Card class="mt-8">
      <div class="space-y-5 p-6">
        {#if step === 0}
          <Input label="Full Name" bind:value={fullName} required
            placeholder="Jane Doe" />
          <Input label="Email" type="email" bind:value={email} required
            placeholder="jane@example.com" />
        {:else if step === 1}
          <RadioGroup bind:value={plan} label="Choose a plan">
            <RadioItem value="starter" label="Starter"
              description="For individuals and side projects" />
            <RadioItem value="pro" label="Professional"
              description="For growing teams" />
            <RadioItem value="enterprise" label="Enterprise"
              description="For large organizations" />
          </RadioGroup>
          <Select label="Region" options={regionOptions}
            bind:value={region} placeholder="Select a region" />
        {:else}
          <Textarea label="Additional Notes" bind:value={notes}
            autoResize showCounter maxlength={500}
            placeholder="Anything else we should know?" />
          <div class="bg-surface-subtle rounded-lg p-4">
            <h4 class="text-text-primary mb-2 text-sm font-semibold">
              Summary
            </h4>
            <dl class="text-text-secondary space-y-1 text-sm">
              <div class="flex justify-between">
                <dt>Name</dt><dd>{fullName}</dd>
              </div>
              <div class="flex justify-between">
                <dt>Email</dt><dd>{email}</dd>
              </div>
              <div class="flex justify-between">
                <dt>Plan</dt><dd>{planLabels[plan] ?? ''}</dd>
              </div>
              <div class="flex justify-between">
                <dt>Region</dt>
                <dd>{regionOptions.find((o) => o.value === region)?.label ?? ''}</dd>
              </div>
            </dl>
          </div>
          <Checkbox label="I agree to the terms and conditions"
            bind:checked={agreedToTerms} />
        {/if}
      </div>
    </Card>

    <div class="mt-6 flex justify-between">
      <Button variant="ghost" intent="neutral"
        onclick={back} disabled={step === 0}>Back</Button>
      <Button intent="primary" onclick={next}
        disabled={!canNext}>
        {step < 2 ? 'Next' : 'Submit'}
      </Button>
    </div>
  {/if}
</div>