Multi-step forms are a good choice when your form is large and has many controls. No one wants to scroll through a super-long form on a mobile device. By grouping controls on a screen-by-screen basis, we can improve the experience of filling out long, complex forms.
But when was the last time you developed a multi-step form? Does that even sound fun to you? There’s so much to think about and so many moving pieces that need to be managed that I wouldn’t blame you for resorting to a form library or even some type of form widget that handles it all for you.
But doing it by hand can be a good exercise and a great way to polish the basics. I’ll show you how I built my first multi-step form, and I hope you’ll not only see how approachable it can be but maybe even spot areas to make my work even better.
We’ll walk through the structure together. We’ll build a job application, which I think many of us can relate to these recent days. I’ll scaffold the baseline HTML, CSS, and JavaScript first, and then we’ll look at considerations for accessibility and validation.
I’ve created a GitHub repo for the final code if you want to refer to it along the way.
The structure of a multi-step form
Our job application form has four sections, the last of which is a summary view, where we show the user all their answers before they submit them. To achieve this, we divide the HTML into four sections, each identified with an ID, and add navigation at the bottom of the page. I’ll give you that baseline HTML in the next section.
Navigating the user to move through sections means we’ll also include a visual indicator for what step they are at and how many steps are left. This indicator can be a simple dynamic text that updates according to the active step or a fancier progress bar type of indicator. We’ll do the former to keep things simple and focused on the multi-step nature of the form.,
The structure and basic styles
We’ll focus more on the logic, but I will provide the code snippets and a link to the complete code at the end.
Let’s start by creating a folder to hold our pages. Then, create an index.html
file and paste the following into it:
Open HTML
<form id="myForm">
<section class="group-one" id="one">
<div class="form-group">
<div class="form-control">
<label for="name">Name <span style="color: red;">*</span></label>
<input type="text" id="name" name="name" placeholder="Enter your name">
</div>
<div class="form-control">
<label for="idNum">ID number <span style="color: red;">*</span></label>
<input type="number" id="idNum" name="idNum" placeholder="Enter your ID number">
</div>
</div>
<div class="form-group">
<div class="form-control">
<label for="email">Email <span style="color: red;">*</span></label>
<input type="email" id="email" name="email" placeholder="Enter your email">
</div>
<div class="form-control">
<label for="birthdate">Date of Birth <span style="color: red;">*</span></label>
<input type="date" id="birthdate" name="birthdate" max="2006-10-01" min="1924-01-01">
</div>
</div>
</section>
<section class="group-two" id="two">
<div class="form-control">
<label for="document">Upload CV <span style="color: red;">*</span></label>
<input type="file" name="document" id="document">
</div>
<div class="form-control">
<label for="department">Department <span style="color: red;">*</span></label>
<select id="department" name="department">
<option value="">Select a department</option>
<option value="hr">Human Resources</option>
<option value="it">Information Technology</option>
<option value="finance">Finance</option>
</select>
</div>
</section>
<section class="group-three" id="three">
<div class="form-control">
<label for="skills">Skills (Optional)</label>
<textarea id="skills" name="skills" rows="4" placeholder="Enter your skills"></textarea>
</div>
<div class="form-control">
<input type="checkbox" name="terms" id="terms">
<label for="terms">I agree to the terms and conditions <span style="color: red;">*</span></label>
</div>
<button id="btn" type="submit">Confirm and Submit</button>
</section>
<div class="arrows">
<button type="button" id="navLeft">Previous</button>
<span id="stepInfo"></span>
<button type="button" id="navRight">Next</button>
</div>
</form>
<script src="script.js"></script>
Looking at the code, you can see three sections and the navigation group. The sections contain form inputs and no native form validation. This is to give us better control of displaying the error messages because native form validation is only triggered when you click the submit button.
Next, create a styles.css
file and paste this into it:
Open base styles
:root
--primary-color: #8c852a;
--secondary-color: #858034;
This dynamically inserts the input values into the summary section of the form, truncates the file names, and offers a fallback text for the input that was not required.
Then update the updateStepVisibility()
function to call the new function:
function updateStepVisibility()
formSteps.forEach((step) =>
document.getElementById(step).style.display = "none";
);
document.getElementById(formSteps[currentStep]).style.display = "block";
stepInfo.textContent = `Step $currentStep + 1 of $formSteps.length`;
if (currentStep === 3)
updateSummaryValues();
navLeft.style.display = currentStep === 0 ? "none" : "block";
navRight.style.display = currentStep === formSteps.length - 1 ? "none" : "block";
Finally, add this to the DOMContentLoaded
event listener:
Object.keys(editButtons).forEach((buttonId) =>
const button = document.getElementById(buttonId);
button.addEventListener("click", (e) =>
currentStep = editButtons[buttonId];
updateStepVisibility();
);
);
Running the form, you should see that the summary section shows all the inputted values and allows the user to edit any before submitting the information:
And now, we can submit our form:
form.addEventListener("submit", (e) =>
e.preventDefault();
if (validateStep(2))
alert("Form submitted successfully!");
form.reset();
currentFormStep = 0;
updateStepVisibility();
);
Our multi-step form now allows the user to edit and see all the information they provide before submitting it.
Accessibility tips
Making multi-step forms accessible starts with the basics: using semantic HTML. This is half the battle. It is closely followed by using appropriate form labels.
Other ways to make forms more accessible include giving enough room to elements that must be clicked on small screens and giving meaningful descriptions to the form navigation and progress indicators.
Offering feedback to the user is an important part of it; it’s not great to auto-dismiss user feedback after a certain amount of time but to allow the user to dismiss it themselves. Paying attention to contrast and font choice is important, too, as they both affect how readable your form is.
Let’s make the following adjustments to the markup for more technical accessibility:
- Add
aria-required="true"
to all inputs except the skills one. This lets screen readers know the fields are required without relying on native validation. - Add
role="alert"
to the error spans. This helps screen readers know to give it importance when the input is in an error state. - Add
role="status" aria-live="polite"
to the.stepInfo
. This will help screen readers understand that the step info keeps tabs on a state, and the aria-live being set to polite indicates that should the value change, it does not need to immediately announce it.
In the script file, replace the showError()
and clearError()
functions with the following:
function showError(input, message)
const formControl = input.parentElement;
const errorSpan = formControl.querySelector(".error-message");
input.classList.add("error");
input.setAttribute("aria-invalid", "true");
input.setAttribute("aria-describedby", errorSpan.id);
errorSpan.textContent = message;
function clearError(input)
const formControl = input.parentElement;
const errorSpan = formControl.querySelector(".error-message");
input.classList.remove("error");
input.removeAttribute("aria-invalid");
input.removeAttribute("aria-describedby");
errorSpan.textContent = "";
Here, we programmatically add and remove attributes that explicitly tie the input with its error span and show that it is in an invalid state.
Finally, let’s add focus on the first input of every section; add the following code to the end of the updateStepVisibility()
function:
const currentStepElement = document.getElementById(formSteps[currentStep]);
const firstInput = currentStepElement.querySelector(
"input, select, textarea"
);
if (firstInput)
firstInput.focus();
And with that, the multi-step form is much more accessible.
Conclusion
There we go, a four-part multi-step form for a job application! As I said at the top of this article, there’s a lot to juggle — so much so that I wouldn’t fault you for looking for an out-of-the-box solution.
But if you have to hand-roll a multi-step form, hopefully now you see it’s not a death sentence. There’s a happy path that gets you there, complete with navigation and validation, without turning away from good, accessible practices.
And this is just how I approached it! Again, I took this on as a personal challenge to see how far I could get, and I’m pretty happy with it. But I’d love to know if you see additional opportunities to make this even more mindful of the user experience and considerate of accessibility.
References
Here are some relevant links I referred to when writing this article:
- How to Structure a Web Form (MDN)
- Multi-page Forms (W3C.org)
- Create accessible forms (A11y Project)
How to Create Multi-Step Forms With Vanilla JavaScript and CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.