1. Web Design
  2. UX/UI
  3. Forms

How to Build a Multi Step Form Wizard with JavaScript

Scroll to top
Read Time: 8 min

In this tutorial, we will build a multi step form using JavaScript, HTML, and CSS (perfect for questionnaires and quizzes).

Filling out long forms can be tedious! As designers we can enhance the experience by focusing on individual components within a multi step form. This design pattern promotes a more user-friendly way to capture user data while sometimes asking for much of it.

The goal with our multi step form will be to reduce the burden of having a more extended form to capture user data, all while ensuring the appropriate data gets submitted.

Our Multi Step Form Demo

Answer the quiz questions and see how our form makes the whole process much more pleasant! Bear in mind that when the form is submitted, nothing currently happens with the data.

1. Start With the HTML

Our HTML markup will effectively give us a tab-based layout. We’ll have three buttons, a form, and some status indicators so a user will be sure what step they are currently on.

1
<div class="container">
2
  <div class="tab-status">
3
    <span class="tab active">1</span>
4
    <span class="tab">2</span>
5
    <span class="tab">3</span>
6
  </div>
7
  <form action="#">
8
    <div role="tab-list">
9
      <div role="tabpanel" id="color" class="tabpanel">
10
        <h3>What is your favorite color?</h3>
11
        <textarea name="color" class="form-input" placeholder="Ruby red"></textarea>
12
      </div>
13
      <div role="tabpanel" id="hobbies" class="tabpanel hidden">
14
        <h3>What are your hobbies?</h3>
15
        <textarea name="hobbies" class="form-input" placeholder="Mountain climbing, Guitar, Skateboarding"></textarea>
16
      </div>
17
        <div role="tabpanel" id="occupation" class="tabpanel hidden">
18
        <h3>What is your occupation?</h3>
19
        <textarea name="occupation" class="form-input" placeholder="Web Designer"></textarea>
20
      </div>
21
    </div>
22
      <div class="pagination">
23
        <a class="btn hidden" id="prev">Previous</a>
24
        <a class="btn" id="next">Continue</a>
25
        <button class="btn btn-submit hidden" id="submit">Submit</button>
26
      </div>
27
    </form>
28
</div>

I’ll start with three questions, but you can extend this to include however many you prefer. The JavaScript code, in the end, is dynamic, which means you can easily add and remove additional questions.

2. Styling the Form With CSS

Without CSS, the multi-step approach doesn’t convey as we’d hope. Here’s the CSS I used to style the HTML.

1
:root {
2
  --color-1: #6366f1;
3
  --color-1-hover: #4338ca;
4
  --color-2: #06b6d4;
5
  --color-2-hover: #0891b2;
6
  --text-color: #312e81;
7
  --status-btn-bg: #f8fafc;
8
  --status-btn-bg-hover: #f1f5f9;
9
}
10
11
body {
12
  background: linear-gradient(to left, var(--color-1), var(--color-2));
13
}
14
15
.container {
16
  margin: 10rem auto;
17
  max-width: 500px;
18
  background: white;
19
  border-radius: 1rem;
20
  padding: 2rem;
21
}
22
23
.form-input {
24
  width: 100%;
25
  border: 1px solid #ddd;
26
  border-radius: .5rem;
27
  box-shadow: inset 0px 1px 2px rgba(0, 0, 0, .1);
28
  padding: 1rem;
29
  box-sizing: border-box;
30
  color: var(--text-color);
31
  transition: ease-in-out .3s all;
32
}
33
34
.form-input::placeholder {
35
  color: #cbd5e1;
36
}
37
38
.form-input:focus {
39
  outline: none;
40
  border-color: var(--color-1);
41
}
42
43
.btn:focus-within,
44
.form-input:focus-within {
45
  box-shadow: #f8fafc 0px 0px 0px 2px, #c7d2fe 0px 0px 0px 6px, #0000 0px 1px 2px 0px;
46
}
47
48
textarea.form-input {
49
  min-height: 150px;
50
}
51
52
.btn {
53
  border: 0;
54
  background: var(--color-1);
55
  padding: 1rem;
56
  border-radius: 25px;
57
  color: white;
58
  cursor: pointer;
59
}
60
61
.btn[disabled] {
62
  opacity: .5;
63
  pointer-events: none;
64
}
65
66
.btn:hover {
67
  background: var(--color-1-hover);
68
  transition: ease-in-out .3s all;
69
}
70
71
.btn-submit {
72
  background-color: var(--color-2);
73
}
74
75
.btn-submit:hover {
76
  background-color: var(--color-2-hover);
77
}
78
79
.pagination {
80
  margin-top: 1rem;
81
  display: flex;
82
  align-items: center;
83
  justify-content: center;
84
}
85
86
.pagination .btn {
87
  width: 100%;
88
  text-align: center;
89
  margin: 0 6px;
90
}
91
92
.tab-status {
93
  display: flex;
94
  align-items: center;
95
}
96
97
.tab-status span {
98
  appearance: none;
99
  background: var(--status-btn-bg);
100
  border: none;
101
  border-radius: 50%;
102
  width: 2rem;
103
  height: 2rem;
104
  margin-right: .5rem;
105
  display: flex;
106
  align-items: center;
107
  justify-content: center;
108
}
109
110
.tab-status span.active {
111
  background-color: var(--color-2);
112
  color: white;
113
}
114
115
.hidden {
116
  display: none;
117
}

3. Onto the JavaScript

Let’s start by declaring some variables. The first three will target the buttons I mentioned previously. We’ll then target the tab panels and tabs as a collection of elements known as a NodeList in JavaScript. That’s a fancy way of calling it an Array.

I created an isEmpty function to help quickly determine if a string value of form input is empty or not.

Finally, there’s the currentStep variable which will change as the Next and Previous buttons are clicked.

1
const previousButton = document.querySelector('#prev')
2
const nextButton = document.querySelector('#next')
3
const submitButton = document.querySelector('#submit')
4
const tabTargets = document.querySelectorAll('.tab')
5
const tabPanels = document.querySelectorAll('.tabpanel')
6
const isEmpty = (str) => !str.trim().length
7
let currentStep = 0

Next and Previous Buttons

Our Next and Previous buttons will be how the user navigates the questionnaire. We’ll leverage the currentStep variable to render the appropriate step and active tab dynamically. Because it returns a number value, we can target the NodeList dynamically.

1
// Next: Change UI relative to the current step and account for button permissions

2
nextButton.addEventListener('click', (event) => {
3
  // Prevent default on links

4
  event.preventDefault()
5
6
  // Hide current tab

7
  tabPanels[currentStep].classList.add('hidden')
8
  tabTargets[currentStep].classList.remove('active')
9
10
  // Show next tab

11
  tabPanels[currentStep + 1].classList.remove('hidden')
12
  tabTargets[currentStep + 1].classList.add('active')
13
  currentStep += 1
14
})

The Next button, once clicked, will signal the HTML/CSS to get busy, hiding the active tab and tab panel and showing the following question in the wizard.

We’ll extend this action to call some more functions. One function will be responsible for updating the status indicators, and the other will validate the user entered a response before they can continue.

Updating Status Dynamically

To update the status, we need to perform a conditional operation that checks the state of the currentStep variable.

Using the tabTargets variable, we can determine how many tabs there are with the .length() method. The length() method dynamically returns the number of tabs in the HTML.

Below, I added comments in the code to better denote what happens after each conditional statement.

1
function updateStatusDisplay() {
2
  // If on the last step, hide the next button and show submit

3
  if (currentStep === tabTargets.length - 1) {
4
    nextButton.classList.add('hidden')
5
    previousButton.classList.remove('hidden')
6
    submitButton.classList.remove('hidden')
7
    validateEntry()
8
9
    // If it's the first step, hide the previous button

10
  } else if (currentStep == 0) {
11
    nextButton.classList.remove('hidden')
12
    previousButton.classList.add('hidden')
13
    submitButton.classList.add('hidden')
14
    // In all other instances, display both buttons

15
  } else {
16
    nextButton.classList.remove('hidden')
17
    previousButton.classList.remove('hidden')
18
    submitButton.classList.add('hidden')
19
  }
20
}

We’ll dynamically show and hide controls relative to the form wizard’s beginning, middle, and end.

Validating User Input

For a multi-step questionnaire/quiz/form to work correctly, we want to ensure data gets adequately submitted.

This tutorial only scratches the surface of what you could validate on the front end, but for now, we are just checking that a value exists for each question.

To extend this functionality, you might check for additional criteria like a length of an answer, if any spam/code entries might otherwise harm the website and more. I’d also advise adding server-side validation so no harmful code enters any database.

1
function validateEntry() {
2
  // Query for the current panel's Textarea input

3
  let input = tabPanels[currentStep].querySelector('.form-input')
4
5
  // Start by disabling the continue and submit buttons

6
  nextButton.setAttribute('disabled', true)
7
  submitButton.setAttribute('disabled', true)
8
9
  // Validate on initial function fire

10
  setButtonPermissions(input)
11
12
  // Validate on input

13
  input.addEventListener('input', () => setButtonPermissions(input))
14
15
  // Validate if blurring from input

16
  input.addEventListener('blur', () => setButtonPermissions(input))
17
}
18
19
function setButtonPermissions(input) {
20
  if (isEmpty(input.value)) {
21
    nextButton.setAttribute('disabled', true)
22
    submitButton.setAttribute('disabled', true)
23
  } else {
24
    nextButton.removeAttribute('disabled')
25
    submitButton.removeAttribute('disabled')
26
  }

I added two functions that work together to help validate that a value is present for each question.

We’ll first validate when the function is initially called and then add a couple of event listener functions to set button permissions dynamically as you interact with the form.

This will disable or enable the Next button and Submit buttons relative to the currentStep variable value and see if there is text present inside each form field.

We add the two functions to the original nextButton function and call them after each click.

1
// Next: Change UI relative to the current step and account for button permissions

2
nextButton.addEventListener('click', (event) => {
3
  event.preventDefault()
4
5
  // Hide current tab

6
  tabPanels[currentStep].classList.add('hidden')
7
  tabTargets[currentStep].classList.remove('active')
8
9
  // Show next tab

10
  tabPanels[currentStep + 1].classList.remove('hidden')
11
  tabTargets[currentStep + 1].classList.add('active')
12
  currentStep += 1
13
14
  validateEntry()
15
  updateStatusDisplay()
16
})

Our previous button resembles the following button logic with slightly different math.

1
// Previous: Change UI relative to the current step and account for button permissions

2
previousButton.addEventListener('click', (event) => {
3
  event.preventDefault()
4
5
  // Hide current tab

6
  tabPanels[currentStep].classList.add('hidden')
7
  tabTargets[currentStep].classList.remove('active')
8
9
  // Show the previous tab

10
  tabPanels[currentStep - 1].classList.remove('hidden')
11
  tabTargets[currentStep - 1].classList.add('active')
12
  currentStep -= 1
13
14
  nextButton.removeAttribute('disabled')
15
  updateStatusDisplay()
16
})

We needn’t call the validateEntry() function on the previous button click as it’s assumed there would already be a value in the form field.

Putting it All Together

Below is the final result (check out the JS tab to see all the code together). The JavaScript code could be more optimized for reusability. Still, it is enough context to help you learn how to build a simple form to navigate, and it makes a user's life easier when it comes to focusing on a specific question and answering it simply.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Web Design tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.