Creating An Effective Multistep Form For Better User Experience
For a multistep form, planning involves structuring questions logically across steps, grouping similar questions, and minimizing the number of steps and the amount of required information for each step. Whatever makes each step focused and manageable is what should be aimed for.
In this tutorial, we will create a multistep form for a job application. Here are the details we are going to be requesting from the applicant at each step:
- Personal Information
Collects applicant’s name, email, and phone number. - Work Experience
Collects the applicant’s most recent company, job title, and years of experience. - Skills & Qualifications
The applicant lists their skills and selects their highest degree. - Review & Submit
This step is not going to collect any information. Instead, it provides an opportunity for the applicant to go back and review the information entered in the previous steps of the form before submitting it.
You can think of structuring these questions as a digital way of getting to know somebody. You can’t meet someone for the first time and ask them about their work experience without first asking for their name.
Based on the steps we have above, this is what the body of our HTML with our form should look like. First, the main <form>
element:
<form id="jobApplicationForm">
<!-- Step 1: Personal Information -->
<!-- Step 2: Work Experience -->
<!-- Step 3: Skills & Qualifications -->
<!-- Step 4: Review & Submit -->
</form>
Step 1 is for filling in personal information, like the applicant’s name, email address, and phone number:
<form id="jobApplicationForm">
<!-- Step 1: Personal Information -->
<fieldset class="step" id="step-1">
<legend id="step1Label">Step 1: Personal Information</legend>
<label for="name">Full Name</label>
<input type="text" id="name" name="name" required />
<label for="email">Email Address</label>
<input type="email" id="email" name="email" required />
<label for="phone">Phone Number</label>
<input type="tel" id="phone" name="phone" required />
</fieldset>
<!-- Step 2: Work Experience -->
<!-- Step 3: Skills & Qualifications -->
<!-- Step 4: Review & Submit -->
</form>
Once the applicant completes the first step, we’ll navigate them to Step 2, focusing on their work experience so that we can collect information like their most recent company, job title, and years of experience. We’ll tack on a new <fieldset>
with those inputs:
<form id="jobApplicationForm">
<!-- Step 1: Personal Information -->
<!-- Step 2: Work Experience -->
<fieldset class="step" id="step-2" hidden>
<legend id="step2Label">Step 2: Work Experience</legend>
<label for="company">Most Recent Company</label>
<input type="text" id="company" name="company" required />
<label for="jobTitle">Job Title</label>
<input type="text" id="jobTitle" name="jobTitle" required />
<label for="yearsExperience">Years of Experience</label>
<input
type="number"
id="yearsExperience"
name="yearsExperience"
min="0"
required
/>
</fieldset>
<!-- Step 3: Skills & Qualifications -->
<!-- Step 4: Review & Submit -->
</form>
Step 3 is all about the applicant listing their skills and qualifications for the job they’re applying for:
<form id="jobApplicationForm">
<!-- Step 1: Personal Information -->
<!-- Step 2: Work Experience -->
<!-- Step 3: Skills & Qualifications -->
<fieldset class="step" id="step-3" hidden>
<legend id="step3Label">Step 3: Skills & Qualifications</legend>
<label for="skills">Skill(s)</label>
<textarea id="skills" name="skills" rows="4" required></textarea>
<label for="highestDegree">Degree Obtained (Highest)</label>
<select id="highestDegree" name="highestDegree" required>
<option value="">Select Degree</option>
<option value="highschool">High School Diploma</option>
<option value="bachelor">Bachelor's Degree</option>
<option value="master">Master's Degree</option>
<option value="phd">Ph.D.</option>
</select>
</fieldset>
<!-- Step 4: Review & Submit -->
<fieldset class="step" id="step-4" hidden>
<legend id="step4Label">Step 4: Review & Submit</legend>
<p>Review your information before submitting the application.</p>
<button type="submit">Submit Application</button>
</fieldset>
</form>
And, finally, we’ll allow the applicant to review their information before submitting it:
<form id="jobApplicationForm">
<!-- Step 1: Personal Information -->
<!-- Step 2: Work Experience -->
<!-- Step 3: Skills & Qualifications -->
<!-- Step 4: Review & Submit -->
<fieldset class="step" id="step-4" hidden>
<legend id="step4Label">Step 4: Review & Submit</legend>
<p>Review your information before submitting the application.</p>
<button type="submit">Submit Application</button>
</fieldset>
</form>
Notice: We’ve added a hidden
attribute to every fieldset
element but the first one. This ensures that the user sees only the first step. Once they are done with the first step, they can proceed to fill out their work experience on the second step by clicking a navigational button. We’ll add this button later on.
To keep things focused, we’re not going to be emphasizing the styles in this tutorial. What we’ll do to keep things simple is leverage the Simple.css style framework to get the form in good shape for the rest of the tutorial.
If you’re following along, we can include Simple’s styles in the document <head>
:
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css" />
And from there, go ahead and create a style.css
file with the following styles that I’ve folded up.
<details>
<summary>View CSS</summary>
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
main {
padding: 0 30px;
}
h1 {
font-size: 1.8rem;
text-align: center;
}
.stepper {
display: flex;
justify-content: flex-end;
padding-right: 10px;
}
form {
box-shadow: 0px 0px 6px 2px rgba(0, 0, 0, 0.2);
padding: 12px;
}
input,
textarea,
select {
outline: none;
}
input:valid,
textarea:valid,
select:valid,
input:focus:valid,
textarea:focus:valid,
select:focus:valid {
border-color: green;
}
input:focus:invalid,
textarea:focus:invalid,
select:focus:invalid {
border: 1px solid red;
}
</details>
Form Navigation And Validation
An easy way to ruin the user experience for a multi-step form is to wait until the user gets to the last step in the form before letting them know of any error they made along the way. Each step of the form should be validated for errors before moving on to the next step, and descriptive error messages should be displayed to enable users to understand what is wrong and how to fix it.
Now, the only part of our form that is visible is the first step. To complete the form, users need to be able to navigate to the other steps. We are going to use several buttons to pull this off. The first step is going to have a Next button. The second and third steps are going to have both a Previous and a Next button, and the fourth step is going to have a Previous and a Submit button.
<form id="jobApplicationForm">
<!-- Step 1: Personal Information -->
<fieldset>
<!-- ... -->
<button type="button" class="next" onclick="nextStep()">Next</button>
</fieldset>
<!-- Step 2: Work Experience -->
<fieldset>
<!-- ... -->
<button type="button" class="previous" onclick="previousStep()">Previous</button>
<button type="button" class="next" onclick="nextStep()">Next</button>
</fieldset>
<!-- Step 3: Skills & Qualifications -->
<fieldset>
<!-- ... -->
<button type="button" class="previous" onclick="previousStep()">Previous</button>
<button type="button" class="next" onclick="nextStep()">Next</button>
</fieldset>
<!-- Step 4: Review & Submit -->
<fieldset>
<!-- ... -->
<button type="button" class="previous" onclick="previousStep()">Previous</button>
<button type="submit">Submit Application</button>
</fieldset>
</form>
Notice: We’ve added onclick
attributes to the Previous and Next buttons to link them to their respective JavaScript functions: previousStep()
and nextStep()
.
The “Next” Button
The nextStep()
function is linked to the Next button. Whenever the user clicks the Next button, the nextStep()
function will first check to ensure that all the fields for whatever step the user is on have been filled out correctly before moving on to the next step. If the fields haven’t been filled correctly, it displays some error messages, letting the user know that they’ve done something wrong and informing them what to do to make the errors go away.
Before we go into the implementation of the nextStep
function, there are certain variables we need to define because they will be needed in the function. First, we need the input fields from the DOM so we can run checks on them to make sure they are valid.
// Step 1 fields
const name = document.getElementById("name");
const email = document.getElementById("email");
const phone = document.getElementById("phone");
// Step 2 fields
const company = document.getElementById("company");
const jobTitle = document.getElementById("jobTitle");
const yearsExperience = document.getElementById("yearsExperience");
// Step 3 fields
const skills = document.getElementById("skills");
const highestDegree = document.getElementById("highestDegree");
Then, we’re going to need an array to store our error messages.
let errorMsgs = [];
Also, we would need an element in the DOM where we can insert those error messages after they’ve been generated. This element should be placed in the HTML just below the last fieldset
closing tag:
<div id="errorMessages" style="color: rgb(253, 67, 67)"></div>
Add the above div
to the JavaScript code using the following line:
const errorMessagesDiv = document.getElementById("errorMessages");
And finally, we need a variable to keep track of the current step.
let currentStep = 1;
Now that we have all our variables in place, here’s the implementation of the nextstep()
function:
function nextStep() {
errorMsgs = [];
errorMessagesDiv.innerText = "";
switch (currentStep) {
case 1:
addValidationErrors(name, email, phone);
validateStep(errorMsgs);
break;
case 2:
addValidationErrors(company, jobTitle, yearsExperience);
validateStep(errorMsgs);
break;
case 3:
addValidationErrors(skills, highestDegree);
validateStep(errorMsgs);
break;
}
}
The moment the Next button is pressed, our code first checks which step the user is currently on, and based on this information, it validates the data for that specific step by calling the addValidationErrors()
function. If there are errors, we display them. Then, the form calls the validateStep()
function to verify that there are no errors before moving on to the next step. If there are errors, it prevents the user from going on to the next step.
Whenever the nextStep()
function runs, the error messages are cleared first to avoid appending errors from a different step to existing errors or re-adding existing error messages when the addValidationErrors
function runs. The addValidationErrors
function is called for each step using the fields for that step as arguments.
Here’s how the addValidationErrors
function is implemented:
function addValidationErrors(fieldOne, fieldTwo, fieldThree = undefined) {
if (!fieldOne.checkValidity()) {
const label = document.querySelector(label[for="${fieldOne.id}"]
);
errorMsgs.push(Please Enter A Valid ${label.textContent}
);
}
if (!fieldTwo.checkValidity()) {
const label = document.querySelector(label[for="${fieldTwo.id}"]
);
errorMsgs.push(Please Enter A Valid ${label.textContent}
);
}
if (fieldThree && !fieldThree.checkValidity()) {
const label = document.querySelector(label[for="${fieldThree.id}"]
);
errorMsgs.push(Please Enter A Valid ${label.textContent}
);
}
if (errorMsgs.length > 0) {
errorMessagesDiv.innerText = errorMsgs.join("
");
}
}
This is how the validateStep()
function is defined:
function validateStep(errorMsgs) {
if (errorMsgs.length === 0) {
showStep(currentStep + 1);
}
}
The validateStep()
function checks for errors. If there are none, it proceeds to the next step with the help of the showStep()
function.
function showStep(step) {
steps.forEach((el, index) => {
el.hidden = index + 1 !== step;
});
currentStep = step;
}
The showStep()
function requires the four fieldsets in the DOM. Add the following line to the top of the JavaScript code to make the fieldsets available:
const steps = document.querySelectorAll(".step");
What the showStep()
function does is to go through all the fieldsets
in our form and hide whatever fieldset
is not equal to the one we’re navigating to. Then, it updates the currentStep
variable to be equal to the step we’re navigating to.
The “Previous” Button
The previousStep()
function is linked to the Previous button. Whenever the previous button is clicked, similarly to the nextStep
function, the error messages are also cleared from the page, and navigation is also handled by the showStep
function.
function previousStep() {
errorMessagesDiv.innerText = "";
showStep(currentStep - 1);
}
Whenever the showStep()
function is called with “currentStep - 1
” as an argument (as in this case), we go back to the previous step, while moving to the next step happens by calling the showStep()
function with “currentStep + 1
" as an argument (as in the case of the validateStep()
function).
One other way of improving the user experience for a multi-step form, is by integrating visual cues, things that will give users feedback on the process they are on. These things can include a progress indicator or a stepper to help the user know the exact step they are on.
Integrating A Stepper
To integrate a stepper into our form (sort of like this one from Material Design), the first thing we need to do is add it to the HTML just below the opening <form>
tag.
<form id="jobApplicationForm">
<div class="stepper">
<span><span class="currentStep">1</span>/4</span>
</div>
<!-- ... -->
</form>
Next, we need to query the part of the stepper that will represent the current step. This is the span tag with the class name of currentStep
.
const currentStepDiv = document.querySelector(".currentStep");
Now, we need to update the stepper value whenever the previous or next buttons are clicked. To do this, we need to update the showStep()
function by appending the following line to it:
currentStepDiv.innerText = currentStep;
This line is added to the showStep()
function because the showStep()
function is responsible for navigating between steps and updating the currentStep
variable. So, whenever the currentStep
variable is updated, the currentStepDiv should also be updated to reflect that change.
Storing And Retrieving User Data
One major way we can improve the form’s user experience is by storing user data in the browser. Multistep forms are usually long and require users to enter a lot of information about themselves. Imagine a user filling out 95% of a form, then accidentally hitting the F5 button on their keyboard and losing all their progress. That would be a really bad experience for the user.
Using localStorage
, we can store user information as soon as it is entered and retrieve it as soon as the DOM content is loaded, so users can always continue filling out their forms from wherever they left off. To add this feature to our forms, we can begin by saving the user’s information as soon as it is typed. This can be achieved using the input
event.
Before adding the input
event listener, get the form element from the DOM:
const form = document.getElementById("jobApplicationForm");
Now we can add the input
event listener:
// Save data on each input event
form.addEventListener("input", () => {
const formData = {
name: document.getElementById("name").value,
email: document.getElementById("email").value,
phone: document.getElementById("phone").value,
company: document.getElementById("company").value,
jobTitle: document.getElementById("jobTitle").value,
yearsExperience: document.getElementById("yearsExperience").value,
skills: document.getElementById("skills").value,
highestDegree: document.getElementById("highestDegree").value,
};
localStorage.setItem("formData", JSON.stringify(formData));
});
Next, we need to add some code to help us retrieve the user data once the DOM content is loaded.
window.addEventListener("DOMContentLoaded", () => {
const savedData = JSON.parse(localStorage.getItem("formData"));
if (savedData) {
document.getElementById("name").value = savedData.name || "";
document.getElementById("email").value = savedData.email || "";
document.getElementById("phone").value = savedData.phone || "";
document.getElementById("company").value = savedData.company || "";
document.getElementById("jobTitle").value = savedData.jobTitle || "";
document.getElementById("yearsExperience").value = savedData.yearsExperience || "";
document.getElementById("skills").value = savedData.skills || "";
document.getElementById("highestDegree").value = savedData.highestDegree || "";
}
});
Lastly, it is good practice to remove data from localStorage
as soon as it is no longer needed:
// Clear data on form submit
form.addEventListener('submit', () => {
// Clear localStorage once the form is submitted
localStorage.removeItem('formData');
});
Adding The Current Step Value To localStorage
If the user accidentally closes their browser, they should be able to return to wherever they left off. This means that the current step value also has to be saved in localStorage
.
To save this value, append the following line to the showStep()
function:
localStorage.setItem("storedStep", currentStep);
Now we can retrieve the current step value and return users to wherever they left off whenever the DOM content loads. Add the following code to the DOMContentLoaded
handler to do so:
const storedStep = localStorage.getItem("storedStep");
if (storedStep) {
const storedStepInt = parseInt(storedStep);
steps.forEach((el, index) => {
el.hidden = index + 1 !== storedStepInt;
});
currentStep = storedStepInt;
currentStepDiv.innerText = currentStep;
}
Also, do not forget to clear the current step value from localStorage
when the form is submitted.
localStorage.removeItem("storedStep");
The above line should be added to the submit handler.
Wrapping UpCreating multi-step forms can help improve user experience for complex data entry. By carefully planning out steps, implementing form validation at each step, and temporarily storing user data in the browser, you make it easier for users to complete long forms.
For the full implementation of this multi-step form, you can access the complete code on GitHub.