Black Friday: Enjoy a 35% discount on the bundles. Apply the code BLACKFRIDAY35 at checkout! Limited offer.
Hello everyone, today we are going to create a dynamic multi-step form using Tailwind CSS and Alpine.js.
These tools make it easy to build responsive, interactive forms without the need for complex JavaScript frameworks. Multi-step forms split long forms into smaller, manageable sections, making the process smoother and less daunting for users.
Dynamic multi-step forms are forms divided into multiple steps or sections, with each section focused on a subset of inputs. Unlike traditional single-page forms, multi-step forms reveal fields in a sequence, allowing users to move forward and backward between steps. Dynamic multi-step forms further enhance this by updating in real-time based on user interactions, adapting elements or even steps based on the data input.
These forms have several benefits, including:
Dynamic multi-step forms are particularly useful in scenarios requiring detailed information or conditional fields. Here are some common use cases:
Online Surveys and Feedback Forms To capture complex feedback, multi-step forms guide users through sections, making it easier to answer a series of questions without feeling overloaded.
User Onboarding When users sign up for a service, multi-step forms can capture detailed information progressively, making it more manageable and improving completion rates.
E-commerce Checkout Online shopping checkouts often use multi-step forms to handle billing, shipping, and payment information separately, simplifying the checkout process for users.
Job Applications Job applications can have multiple sections, like personal information, employment history, and skills. Using a multi-step format helps ensure users don’t miss important sections.
Event Registrations For event sign-ups, attendees may need to provide attendee details, dietary restrictions, accommodation preferences, etc. Multi-step forms make it easy to handle each segment independently.
We are going to create an Alpine object on the main container
x-data="{ }"
: This is the data object that holds the form data and the current step.currentStep: 1
: This is the current step of the form.formData: { name: '', email: '', interests: [], experience: '' }
: This is the form data object that holds the user’s name, email, interests, and experience.errors: {}
: This is an object that holds any errors that occur during form validation.validateStep()
: This is a function that validates the form data and updates the errors object.if (this.currentStep === 1)
: This is a conditional statement that checks if the current step is 1.if (!this.formData.name)
: This is a conditional statement that checks if the name field is empty.this.errors.name = 'Name is required'
: This is an error message that is displayed if the name field is empty.if (!this.formData.email)
: This is a conditional statement that checks if the email field is empty.this.errors.email = 'Email is required'
: This is an error message that is displayed if the email field is empty.else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.formData.email))
: This is a conditional statement that checks if the email field is in the correct format.this.errors.email = 'Invalid email format'
: This is an error message that is displayed if the email field is in the wrong format.if (this.currentStep === 2 && this.formData.interests.length === 0)
: This is a conditional statement that checks if the current step is 2 and the interests field is empty.this.errors.interests = 'Select at least one interest'
: This is an error message that is displayed if the interests field is empty.return Object.keys(this.errors).length === 0
: This is a conditional statement that checks if there are no errors in the errors object.x - data = "{
currentStep: 1,
formData: {
name: '',
email: '',
interests: [],
experience: ''
},
errors: {},
validateStep() {
this.errors = {};
if (this.currentStep === 1) {
if (!this.formData.name) {
this.errors.name = 'Name is required';
}
if (!this.formData.email) {
this.errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.formData.email)) {
this.errors.email = 'Invalid email format';
}
}
if (this.currentStep === 2 && this.formData.interests.length === 0) {
this.errors.interests = 'Select at least one interest';
}
return Object.keys(this.errors).length === 0;
}
}
"
Let’s break down the markup for each step. Irrelevant classes have been remover for brevity, but I’ll keep those classes relevant to github code.
The firs div is the progress bar, it’s a div with a background color and a width that is set to the current step multiplied by 50%.
:style="
width: ${((currentStep - 1) * 50)}%"
: This is a dynamic expression that sets the width of the progress bar to the current step multiplied by 50%.The second div is the step labels, it’s 3 different spans that display the step labels based on the current step.
:class="{ 'text-blue-600 font-medium': currentStep === 1 }"
: This is a dynamic expression that sets the text color and font weight of the span to blue-600 and medium, respectively, if the current step is 1.:class="{ 'text-blue-600 font-medium': currentStep === 2 }"
: This is a dynamic expression that sets the text color and font weight of the span to blue-600 and medium, respectively, if the current step is 2.:class="{ 'text-blue-600 font-medium': currentStep === 3 }"
: This is a dynamic expression that sets the text color and font weight of the span to blue-600 and medium, respectively, if the current step is 3.<div>
<div
class="bg-blue-500..."
:style="`width: ${((currentStep - 1) * 50)}%`">
</div>
</div>
<div>
<span :class="{ 'text-blue-600 font-medium': currentStep === 1 }"
>Personal Info</span
>
<span :class="{ 'text-blue-600 font-medium': currentStep === 2 }"
>Interests</span
>
<span :class="{ 'text-blue-600 font-medium': currentStep === 3 }"
>Experience</span
>
</div>
The input field
type="text"
: This is the type of input field.x-model="formData.name"
: This is a directive that binds the value of the input field to the formData.name
property in the data object.:class="{'border-red-500 focus:border-red-500': errors.name}"
: This is a dynamic expression that adds the border-red-500
class to the input field if the errors.name
property is truthy.
The error messagex-show="errors.name"
: This is a directive that shows the error message if the errors.name
property is truthy.x-text="errors.name"
: This is a directive that displays the value of the errors.name
property as the error message.class="text-red-500 text-sm"
: This is a class that sets the text color and font size of the error message to red-500 and small, respectively.<div x-show="currentStep === 1" x-cloak>
<h2>Personal Information</h2>
<div>
<label>Name</label>
<input type="text" x-model="formData.name" :class="{'border-red-500 focus:border-red-500': errors.name}" class="..." />
<span x-show="errors.name" x-text="errors.name" class="text-red-500 text-sm"></span>
</div>
<div>
<label>Email</label>
<input type="email" x-model="formData.email" :class="{'!border-red-500 ring-red-500': errors.email}" class="..." />
<span x-show="errors.email" x-text="errors.email" class="text-red-500 text-sm"></span>
</div>
<button @click="validateStep() && currentStep++"> Next Step </button>
</div>
The wrapper
x-show="currentStep === 2"
: This is a directive that shows the wrapper if the current step is 2.
The checkboxesinput type="checkbox" x-model="formData.interests" value="web"
: This is an input field of type checkbox that binds the value of the checkbox to the formData.interests
property in the data object.: This is an input field of type checkbox that binds the value of the checkbox to the
formData.interests` property in the data object.: This is an input field of type checkbox that binds the value of the checkbox to the
formData.interests` property in the data object.
The error messagex-show="errors.interests"
: This is a directive that shows the error message if the errors.interests
property is truthy.
The buttons@click="currentStep--"
: This is an event handler that decrements the current step by 1.@click="validateStep() && currentStep++"
: This is an event handler that validates the form data and increments the current step by 1.<div x-show="currentStep === 2" x-cloak>
<h2>Your Interests</h2>
<div>
<label>Select your interests</label>
<div>
<label>
<input type="checkbox" x-model="formData.interests" value="web" />
<span>Web Development</span>
</label>
<label>
<input type="checkbox" x-model="formData.interests" value="mobile" />
<span>Mobile Development</span>
</label>
<label>
<input type="checkbox" x-model="formData.interests" value="data" />
<span>Data Science</span>
</label>
</div>
<span x-show="errors.interests" x-text="errors.interests" class="text-red-500 text-sm"></span>
</div>
<div>
<button @click="currentStep-->">
Back
</button>
<button @click=" validateStep() && currentStep++">
Next Step
</button>
</div>
</div>
The textarea
x-model="formData.experience"
: This is a directive that binds the value of the textarea to the formData.experience
property in the data object.
The buttons@click="currentStep--"
: This is an event handler that decrements the current step by 1.@click="alert('Form submitted with: ' + JSON.stringify(formData, null, 2))"
: This is an event handler that displays an alert with the form data.<div x-show="currentStep === 3" x-cloak>
<h2>Your Experience</h2>
<div>
<label>Tell us about your experience</label>
<textarea x-model="formData.experience"></textarea>
</div>
<div>
<button @click="currentStep--">
Back
</button>
<button @click="alert('Form submitted with: ' + JSON.stringify(formData, null, 2))">
Submit
</button>
</div>
</div>
<div x-data="{
currentStep: 1,
formData: {
name: '',
email: '',
interests: [],
experience: ''
},
errors: {},
validateStep() {
this.errors = {};
if (this.currentStep === 1) {
if (!this.formData.name) {
this.errors.name = 'Name is required';
}
if (!this.formData.email) {
this.errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.formData.email)) {
this.errors.email = 'Invalid email format';
}
}
if (this.currentStep === 2 && this.formData.interests.length === 0) {
this.errors.interests = 'Select at least one interest';
}
return Object.keys(this.errors).length === 0;
}
}">
<!-- Progress Bar -->
<div>
<div>
<div class="bg-blue-500 h-2 rounded-full transition-all duration-300" :style="`width: ${((currentStep - 1) * 50)}%`"></div>
</div>
<div>
<span :class="{ 'text-blue-600 font-medium': currentStep === 1 }">Personal Info</span>
<span :class="{ 'text-blue-600 font-medium': currentStep === 2 }">Interests</span>
<span :class="{ 'text-blue-600 font-medium': currentStep === 3 }">Experience</span>
</div>
</div>
<!-- Step 1: Personal Information -->
<div x-show="currentStep === 1" x-cloak>
<h2>Personal Information</h2>
<div>
<label>Name</label>
<input type="text" x-model="formData.name" :class="{'border-red-500 focus:border-red-500': errors.name}" class="..." />
<span x-show="errors.name" x-text="errors.name" class="text-red-500 text-sm"></span>
</div>
<div>
<label>Email</label>
<input type="email" x-model="formData.email" :class="{'!border-red-500 ring-red-500': errors.email}" class="..." />
<span x-show="errors.email" x-text="errors.email" class="text-red-500 text-sm"></span>
</div>
<button @click="validateStep() && currentStep++"> Next Step </button>
</div>
<!-- Step 2: Interests -->
<div x-show="currentStep === 2" x-cloak>
<h2>Your Interests</h2>
<div>
<label>Select your interests</label>
<div>
<label>
<input type="checkbox" x-model="formData.interests" value="web" />
<span>Web Development</span>
</label>
<label>
<input type="checkbox" x-model="formData.interests" value="mobile" />
<span>Mobile Development</span>
</label>
<label>
<input type="checkbox" x-model="formData.interests" value="data" />
<span>Data Science</span>
</label>
</div>
<span x-show="errors.interests" x-text="errors.interests" class="text-red-500 text-sm"></span>
</div>
<div>
<button @click="currentStep--"> Back </button>
<button @click="validateStep() && currentStep++"> Next Step </button>
</div>
</div>
<!-- Step 3: Experience -->
<div x-show="currentStep === 3" x-cloak>
<h2>Your Experience</h2>
<div>
<label>Tell us about your experience</label>
<textarea x-model="formData.experience"></textarea>
</div>
<div>
<button @click="currentStep--"> Back </button>
<button @click="alert('Form submitted with: ' + JSON.stringify(formData, null, 2))"> Submit </button>
</div>
</div>
</div>
This is a simple multi-step form that demonstrates how to use Tailwind CSS and Alpine.js to create a dynamic multi-step form with validation and error handling.
Hope you enjoyed this tutorial and have a great day!
/Michael Andreuzza
Own all themes forever for $199.
Includes new themes, updates, unlimited projects, and lifetime support. — No subscription required!