← Back to all tutorials

How to create a dynamic multiple step form with Tailwind CSS and Alpine JS

a form
Published and written on Oct 30 2024 by Michael Andreuzza

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.

What are Dynamic Multi-Step Forms?

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:

  • Improved user engagement: They feel less overwhelming since users are shown only a few fields at a time.
  • Reduced error rate: By organizing fields logically, users are less likely to make mistakes or miss fields.
  • Data-driven flexibility: Dynamic multi-step forms can adjust fields and content in real-time, creating a personalized user experience.

Use Cases for Dynamic Multi-Step Forms

Dynamic multi-step forms are particularly useful in scenarios requiring detailed information or conditional fields. Here are some common use cases:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

Let’s get started with the code:

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;
   }
}
"

Now the markup

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 progress bar

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 step labels

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>

Step 1: Personal Information

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 message
  • x-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>

Step 2: Interests

The wrapper

  • x-show="currentStep === 2": This is a directive that shows the wrapper if the current step is 2. The checkboxes
  • input 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.
  • ìnput type=“checkbox” x-model=“formData.interests” value=“mobile”: This is an input field of type checkbox that binds the value of the checkbox to the formData.interests` property in the data object.
  • ìnput type=“checkbox” x-model=“formData.interests” value=“data”: 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 message
  • x-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>

Step 3: Experience

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>

The full markup

<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>

Conclusion

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

Did you like this tutorial? Please share it with your friends!

Get lifetime access to every theme available today for $199 $139 and own them forever. Plus, new themes, lifetime updates, use on unlimited projects and enjoy lifetime support.

— No subscription required!