Multi-Step Registration Form Component
A modern, multi-step registration form that breaks down the sign-up process into logical sections: Personal, Account, and Preferences. It features a dynamic progress bar, step-by-step validation, smooth transitions between steps, and a final success screen. This user-friendly approach improves user experience for longer forms.
LTR
RTL
<section class="relative overflow-hidden min-h-[100vh] bg-white dark:bg-gray-900 flex items-center justify-center py-12">
<!-- Background elements -->
<div class="absolute inset-0 overflow-hidden">
<div class="absolute -top-1/2 -right-1/2 rtl:-left-1/2 rtl:right-auto bg-blue-400/20 dark:bg-blue-600/20 blur-3xl w-[384px] h-[384px] rounded-full"></div>
<div class="absolute -bottom-1/2 -left-1/2 rtl:-right-1/2 rtl:left-auto bg-purple-400/20 dark:bg-purple-600/20 blur-3xl w-[384px] h-[384px] rounded-full"></div>
</div>
<div class="relative z-10 mx-auto px-4 w-full max-w-2xl">
<!-- Logo/Brand -->
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 leading-tight sm:text-4xl dark:text-white">Create Account</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">Join us and build the future today</p>
</div>
<!-- Multi-step Form Container -->
<div class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-2xl shadow-xl p-8 border border-gray-200/50 dark:border-gray-700/50"
x-data="{
currentStep: 1,
submitting: false,
showPassword: false,
showConfirmPassword: false,
steps: [
{ title: 'Personal', completed: false },
{ title: 'Account', completed: false },
{ title: 'Preferences', completed: false }
],
formData: {
firstName: '',
lastName: '',
email: '',
phone: '',
username: '',
password: '',
confirmPassword: '',
newsletter: true,
marketing: false,
accountType: 'personal',
terms: false
},
errors: {},
validateStep(step) {
this.errors = {};
if (step === 1) {
if (!this.formData.firstName.trim()) this.errors.firstName = 'First name is required';
if (!this.formData.lastName.trim()) this.errors.lastName = 'Last name is required';
if (!this.formData.email.trim()) {
this.errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.formData.email)) {
this.errors.email = 'Please enter a valid email address';
}
}
if (step === 2) {
if (!this.formData.username.trim()) this.errors.username = 'Username is required';
if (!this.formData.password) {
this.errors.password = 'Password is required';
} else if (this.formData.password.length < 8) {
this.errors.password = 'Password must be at least 8 characters';
}
if (this.formData.password !== this.formData.confirmPassword) {
this.errors.confirmPassword = 'Passwords do not match';
}
}
if (step === 3) {
if (!this.formData.terms) {
this.errors.terms = 'You must accept the terms and conditions';
}
}
return Object.keys(this.errors).length === 0;
},
nextStep() {
if (this.validateStep(this.currentStep)) {
this.steps[this.currentStep - 1].completed = true;
this.currentStep++;
}
},
prevStep() {
if (this.currentStep > 1) {
this.currentStep--;
}
},
async submitForm() {
if (!this.validateStep(3)) return;
this.submitting = true;
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
// Move to success step
this.currentStep = 4;
this.submitting = false;
},
// Calculate the progress line width
getProgressWidth() {
if (this.currentStep === 1) return '0%';
if (this.currentStep === 2) return '50%';
if (this.currentStep === 3 || this.currentStep === 4) return '100%';
return '0%';
},
// Check if step is completed
isStepCompleted(index) {
return index < this.currentStep - 1;
},
// Check if step is active
isStepActive(index) {
return index === this.currentStep - 1;
}
}">
<!-- Progress Steps -->
<div class="mb-8">
<div class="flex justify-between relative">
<!-- Progress Line -->
<div class="absolute top-4 left-0 right-0 h-0.5 bg-gray-200 dark:bg-gray-700 -z-10">
<div class="h-full bg-blue-600 transition-all duration-500 ease-in-out"
x-bind:style="`width: ${getProgressWidth()}`"></div>
</div>
<!-- Step Indicators -->
<template x-for="(step, index) in steps" :key="index">
<div class="flex flex-col items-center relative z-10">
<div class="w-8 h-8 rounded-full flex items-center justify-center transition-all duration-300 border-2"
:class="{
'bg-blue-600 border-blue-600 text-white': isStepCompleted(index) || isStepActive(index),
'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-500': !isStepCompleted(index) && !isStepActive(index)
}">
<span x-text="index + 1"
:class="{
'text-white': isStepCompleted(index) || isStepActive(index),
'text-gray-500': !isStepCompleted(index) && !isStepActive(index)
}"></span>
</div>
<span class="mt-2 text-sm font-medium"
:class="{
'text-blue-600': isStepCompleted(index) || isStepActive(index),
'text-gray-500': !isStepCompleted(index) && !isStepActive(index)
}" x-text="step.title"></span>
</div>
</template>
</div>
</div>
<!-- Multi-step Form -->
<form @submit.prevent="submitForm">
<!-- Step 1: Personal Information -->
<div x-show="currentStep === 1" x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform translate-x-8"
x-transition:enter-end="opacity-100 transform translate-x-0"
class="space-y-6">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Personal Information</h2>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="firstName" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">First Name</label>
<input type="text" id="firstName" x-model="formData.firstName" class="w-full form-input px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200" placeholder="John" required>
<div x-show="errors.firstName" x-text="errors.firstName" class="mt-1 text-sm text-red-600 dark:text-red-400"></div>
</div>
<div>
<label for="lastName" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Last Name</label>
<input type="text" id="lastName" x-model="formData.lastName" class="w-full form-input px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200" placeholder="Doe" required>
<div x-show="errors.lastName" x-text="errors.lastName" class="mt-1 text-sm text-red-600 dark:text-red-400"></div>
</div>
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email Address</label>
<input type="email" id="email" x-model="formData.email" class="w-full form-input px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200" placeholder="you@example.com" required>
<div x-show="errors.email" x-text="errors.email" class="mt-1 text-sm text-red-600 dark:text-red-400"></div>
</div>
<div>
<label for="phone" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Phone Number</label>
<input type="tel" id="phone" x-model="formData.phone" class="w-full form-input px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200" placeholder="+1 (555) 123-4567">
<div x-show="errors.phone" x-text="errors.phone" class="mt-1 text-sm text-red-600 dark:text-red-400"></div>
</div>
<div class="flex justify-end">
<button type="button" @click="nextStep" class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200 font-medium">
Continue
</button>
</div>
</div>
<!-- Step 2: Account Setup -->
<div x-show="currentStep === 2" x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform translate-x-8"
x-transition:enter-end="opacity-100 transform translate-x-0"
class="space-y-6">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Account Setup</h2>
<div>
<label for="username" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Username</label>
<input type="text" id="username" x-model="formData.username" class="w-full form-input px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200" placeholder="johndoe" required>
<div x-show="errors.username" x-text="errors.username" class="mt-1 text-sm text-red-600 dark:text-red-400"></div>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label>
<div class="relative">
<input :type="showPassword ? 'text' : 'password'" id="password" x-model="formData.password" class="w-full form-input px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200 pr-10" placeholder="••••••••" required>
<button type="button" @click="showPassword = !showPassword" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">
<svg x-show="!showPassword" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
<svg x-show="showPassword" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>
</svg>
</button>
</div>
<div x-show="errors.password" x-text="errors.password" class="mt-1 text-sm text-red-600 dark:text-red-400"></div>
</div>
<div>
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Confirm Password</label>
<div class="relative">
<input :type="showConfirmPassword ? 'text' : 'password'" id="confirmPassword" x-model="formData.confirmPassword" class="w-full form-input px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200 pr-10" placeholder="••••••••" required>
<button type="button" @click="showConfirmPassword = !showConfirmPassword" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">
<svg x-show="!showConfirmPassword" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
<svg x-show="showConfirmPassword" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>
</svg>
</button>
</div>
<div x-show="errors.confirmPassword" x-text="errors.confirmPassword" class="mt-1 text-sm text-red-600 dark:text-red-400"></div>
</div>
<div class="flex justify-between">
<button type="button" @click="prevStep" class="px-6 py-3 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200 font-medium">
Back
</button>
<button type="button" @click="nextStep" class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200 font-medium">
Continue
</button>
</div>
</div>
<!-- Step 3: Preferences & Finalization -->
<div x-show="currentStep === 3" x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform translate-x-8"
x-transition:enter-end="opacity-100 transform translate-x-0"
class="space-y-6">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Preferences & Finalization</h2>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Communication Preferences</label>
<div class="space-y-3">
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="newsletter" x-model="formData.newsletter" type="checkbox" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
</div>
<label for="newsletter" class="ml-2 text-sm text-gray-700 dark:text-gray-300">
Send me product updates and newsletters
</label>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="marketing" x-model="formData.marketing" type="checkbox" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
</div>
<label for="marketing" class="ml-2 text-sm text-gray-700 dark:text-gray-300">
Send me marketing communications
</label>
</div>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Account Type</label>
<div class="grid grid-cols-2 gap-4">
<div>
<input type="radio" id="personal" name="accountType" x-model="formData.accountType" value="personal" class="hidden peer">
<label for="personal" class="flex flex-col p-4 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer peer-checked:border-blue-600 peer-checked:bg-blue-50 dark:peer-checked:bg-blue-900/20 transition-colors duration-200">
<span class="font-medium text-gray-900 dark:text-white">Personal</span>
<span class="text-sm text-gray-600 dark:text-gray-400">For individual use</span>
</label>
</div>
<div>
<input type="radio" id="business" name="accountType" x-model="formData.accountType" value="business" class="hidden peer">
<label for="business" class="flex flex-col p-4 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer peer-checked:border-blue-600 peer-checked:bg-blue-50 dark:peer-checked:bg-blue-900/20 transition-colors duration-200">
<span class="font-medium text-gray-900 dark:text-white">Business</span>
<span class="text-sm text-gray-600 dark:text-gray-400">For teams and organizations</span>
</label>
</div>
</div>
</div>
<div class="mb-6">
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="terms" x-model="formData.terms" type="checkbox" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" required>
</div>
<label for="terms" class="ml-2 text-sm text-gray-700 dark:text-gray-300">
I agree to the <a href="#" class="text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300">Terms and Conditions</a> and <a href="#" class="text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300">Privacy Policy</a>
</label>
</div>
<div x-show="errors.terms" x-text="errors.terms" class="mt-1 text-sm text-red-600 dark:text-red-400"></div>
</div>
<div class="flex justify-between">
<button type="button" @click="prevStep" class="px-6 py-3 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200 font-medium">
Back
</button>
<button type="submit" :disabled="submitting" class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-70 disabled:cursor-not-allowed transition-colors duration-200 font-medium flex items-center">
<span x-show="!submitting">Create Account</span>
<span x-show="submitting" class="flex items-center">
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Creating Account...
</span>
</button>
</div>
</div>
<!-- Success Message -->
<div x-show="currentStep === 4" x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100"
class="text-center py-8">
<div class="mb-4 flex justify-center">
<div class="w-16 h-16 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center">
<svg class="w-8 h-8 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Account Created Successfully!</h3>
<p class="text-gray-600 dark:text-gray-400 mb-6">Welcome to our platform! We've sent a confirmation email to your address.</p>
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 mb-6 text-left">
<h4 class="font-medium text-blue-800 dark:text-blue-300 mb-2">Your Account Details:</h4>
<p class="text-sm text-blue-700 dark:text-blue-400" x-text="`Username: ${formData.username}`"></p>
<p class="text-sm text-blue-700 dark:text-blue-400" x-text="`Email: ${formData.email}`"></p>
<p class="text-sm text-blue-700 dark:text-blue-400" x-text="`Account Type: ${formData.accountType === 'personal' ? 'Personal' : 'Business'}`"></p>
</div>
<a href="#" class="inline-block px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200 font-medium">Go to Dashboard</a>
</div>
</form>
</div>
</div>
</section>