Skip to content

Multi-Step Form Wizard

A linear process for complex tasks, breaking down long forms into manageable steps to improve user completion rates and reduce cognitive load.

Demo

Implementation

vue
<script setup lang="ts">
import { ref, computed } from 'vue';

// 1. Component Imports (Default)
import CLCard from '@codeandfunction/callaloo/CLCard';
import CLProgress from '@codeandfunction/callaloo/CLProgress';
import CLInput from '@codeandfunction/callaloo/CLInput';
import CLSelect from '@codeandfunction/callaloo/CLSelect';
import CLButton from '@codeandfunction/callaloo/CLButton';
import CLHeading from '@codeandfunction/callaloo/CLHeading';
import CLText from '@codeandfunction/callaloo/CLText';
import CLIcon from '@codeandfunction/callaloo/CLIcon';

import { useToast } from '@codeandfunction/callaloo/composables/useToast';

import type { CLOption } from '@codeandfunction/callaloo';

// 2. Enum & Type Imports (Named)
import { 
  CLButtonTypes,
  CLColorVariants,
  CLColors, 
  CLHeadingLevels,
  CLHeadingTypes,
  CLIconNames,
  CLInputTypes,
  CLTextTypes,
} from '@codeandfunction/callaloo';

// 3. State & Logic
const currentStep = ref(1);
const totalSteps = 3;
const isSubmitting = ref(false);
const toast = useToast();

interface WizardFormData {
  email: string;
  fullName: string;
  password: string;
  plan: string;
}

const formData = ref<WizardFormData>({
  email: '',
  fullName: '',
  password: '',
  plan: 'hobby',
});

const progressValue = computed(() => {
  return (currentStep.value / totalSteps) * 100;
});

const planOptions: CLOption[] = [
  { label: 'Hobby (Free)', value: 'hobby' },
  { label: 'Pro ($19/mo)', value: 'pro' },
  { label: 'Enterprise (Custom)', value: 'enterprise' },
];

const nextStep = (): void => {
  if (currentStep.value < totalSteps) {
    currentStep.value++;
  }
};

const prevStep = (): void => {
  if (currentStep.value > 1) {
    currentStep.value--;
  }
};

const resetWizard = (): void => {
  currentStep.value = 1;
};

const handleSubmit = async (): Promise<void> => {
  isSubmitting.value = true;
  await new Promise(resolve => setTimeout(resolve, 2000));
  isSubmitting.value = false;
  toast.showToast({
    color: CLColors.Success,
    message: 'Welcome to Callaloo! Your account is ready.',
    title: 'Account Created',
  });
  currentStep.value = 4; // Success
};
</script>

<template>
  <div class="pattern-preview">
    <CLCard :variant="CLColorVariants.Ghost" :padded="true" width="300px" elevated>
      <div v-if="currentStep <= totalSteps">
        <div class="step-header">
          <div class="step-indicator">
            <CLText :type="CLTextTypes.Small" :color="CLColors.Neutral">Step {{ currentStep }} of {{ totalSteps }}</CLText>
            <CLText :type="CLTextTypes.Small" :color="CLColors.Primary" medium>
              {{ Math.round(progressValue) }}% Complete
            </CLText>
          </div>
          <CLProgress :model-value="progressValue" :color="CLColors.Primary" :rounded="true" />
        </div>
        <div class="form-body">
          <template v-if="currentStep === 1">
            <CLHeading :level="CLHeadingLevels.H3" :type="CLHeadingTypes.Large">Account Information</CLHeading>
            <CLInput id="email" name="email" label="Email Address" v-model="formData.email" placeholder="[email protected]" :type="CLInputTypes.Email" fluid />
            <CLInput id="pass" name="pass" label="Password" v-model="formData.password" :type="CLInputTypes.Password" fluid />
          </template>
          <template v-if="currentStep === 2">
            <CLHeading :level="CLHeadingLevels.H3" :type="CLHeadingTypes.Large">Profile Details</CLHeading>
            <CLInput id="name" name="name" label="Full Name" v-model="formData.fullName" placeholder="John Doe" fluid />
            <CLSelect id="plan" name="plan" label="Select Plan" v-model="formData.plan" :options="planOptions" fluid />
          </template>
          <template v-if="currentStep === 3">
            <CLHeading :level="CLHeadingLevels.H3" :type="CLHeadingTypes.Large">Review & Confirm</CLHeading>
            <div class="review-panel">
              <CLText :type="CLTextTypes.Small"><strong>Email:</strong> {{ formData.email || 'Not provided' }}</CLText>
              <CLText :type="CLTextTypes.Small"><strong>Name:</strong> {{ formData.fullName || 'Not provided' }}</CLText>
              <CLText :type="CLTextTypes.Small"><strong>Plan:</strong> {{ formData.plan }}</CLText>
            </div>
            <CLText :type="CLTextTypes.Small" :color="CLColors.Neutral">By clicking submit, you agree to our terms of service.</CLText>
          </template>
        </div>
        <div class="form-footer">
          <CLButton 
            :variant="CLColorVariants.Ghost" 
            :color="CLColors.Neutral"
            :type="CLButtonTypes.Button"
            @click="prevStep"
            :disabled="currentStep === 1 || isSubmitting"
          >
            Previous
          </CLButton>
          <CLButton 
            v-if="currentStep < totalSteps"
            :color="CLColors.Primary" 
            :type="CLButtonTypes.Button"
            @click="nextStep"
          >
            Continue
          </CLButton>
          <CLButton 
            v-else
            :color="CLColors.Success" 
            :type="CLButtonTypes.Button"
            @click="handleSubmit"
            :busy="isSubmitting"
          >
            Create Account
          </CLButton>
        </div>
      </div>
      <div v-else class="success-state">
        <CLIcon :name="CLIconNames.CircleCheck" :size="64" :color="CLColors.Success" />
        <CLHeading :level="CLHeadingLevels.H3" :type="CLHeadingTypes.Large">Setup Complete!</CLHeading>
        <CLText :color="CLColors.Neutral">Your account has been successfully created. You can now start using Callaloo.</CLText>
        <CLButton :color="CLColors.Primary" :type="CLButtonTypes.Button" @click="resetWizard">Return to Start</CLButton>
      </div>
    </CLCard>
  </div>
</template>

<style scoped>
  .pattern-preview {
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 0 auto;
    max-width: 300px;
    flex: 1;
  }
  .step-header {
    margin-bottom: 2rem;
  }
  .step-indicator {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 0.5rem;
  }
  .review-panel {
    background: var(--vp-c-bg-alt);
    border-radius: 8px;
    padding: 1rem;
  }
  .form-body {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
    min-height: 220px;
  }
  .form-footer {
    display: flex;
    justify-content: space-between;
    margin-top: 2rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--vp-c-divider);
  }
  .success-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 1.5rem;
  }
</style>

Released under the MIT License.