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>