How To Build a Multi-Step Form

Sometimes, forms are too long and can be intimidating to complete. The go-to solution is to create a multi-step form, where the form is broken up into smaller sections that the user completes progressively.

This tutorial will teach you how to build such a form using simple vanilla Javascript, and very minimal CSS. Here’s a sneak peak of what we’ll build:

Let’s get started.

Before we dive into each step, here’s the roadmap of everything we’ll need to do, in order:

  1. Create the entire form with all the fields on one page (HTML)
  2. Add some initial Javascript to make certain fields appear and disappear based on user answers (HTML+JS)
  3. Carve up the form into the various pages / sections we want (HTML)
  4. Add more Javascript to power the multi-step functionality (HTML+JS)
  5. (Optional) Spice things up with a Bootstrap progress bar and more eye candy (CSS)

1. Create the Form

Let’s start with our basic, all-in-one form below.

Nothing fancy in our example form here: just a nice variety of different types of fields that include input texts, radio buttons, select/dropdowns, input numbers, checkboxes, and a text area.

There’s only one final submit button, at the very end.

<form name="multistep-form" method="POST">
<div id="empty"></div>

<p>
<label>Gender: </label>
<label>Male&nbsp;<input type="radio" name="gender" id= "male" value= "M" required></label>&nbsp;&nbsp;
<label>Female&nbsp;<input type="radio" name="gender" id= "female" value = "F"  required></label>
</p>

<p>
<label for="inputBirthday">Birthday: </label>
<input type="text" name="birthday" id="inputBirthday" maxlength="10" pattern="(0[1-9]|1[012])/(0[1-9]|[12][0-9]|3[01])/(19|20)\d\d" title="Birthday must follow format: MM/DD/YYYY" placeholder="MM/DD/YYYY" required>
</p>
<p>
<label>Height: </label>
<input type="number" name="height-feet" id="height-feet" placeholder="ft" maxlength="1" max="8" min="3" required>&nbsp;&nbsp;
<input type="number" name="height-inches" id="height-inches" maxlength="2" max="11" min="0" placeholder="in" required>
</p>
<p>
<label for="inputWeight">Weight (lbs): </label>
<input type="number" name="inputWeight" id="inputWeight" placeholder="lbs" maxlength="3" max="999" min="100" required>
</p>

<p>
Do you currently have a life insurance policy?
</p>
<p>
<label>Yes&nbsp;<input type="radio" name="alreadyHasInsurance" id= "insuranceYes" value= "Y" required></label> &nbsp;&nbsp;

<label>No&nbsp;<input type="radio" name="alreadyHasInsurance" id= "insuranceNo" value = "N" required></label>
</p>

<p>
<label for="currentPolicyAmount">Current policy coverage amount ($US): &nbsp;</label>
<input type="number" name="currentPolicyAmount" id="currentPolicyAmount" placeholder="" min=1 >
</p>

<label for="newCoverageAmount" >How much coverage would you like us to quote you? ($US) </label> <small>It's OK to estimate.</small>
<p>
<select name="newCoverageAmount" id="newCoverageAmount" required>
<option value="" selected="selected">Please select</option>
<option value="<200K">$0 - $199,999</option>
<option value="200K-300K">$200,000 - $299,999</option>
<option value="300K-400K">$300,000 - $399,999</option>
<option value="400K-500K">$400,000 - $499,999</option>
<option value="500K-600K">$500,000 - $599,999</option>
<option value="600K-700K">$600,000 - $699,999</option>
<option value="700K-800K">$700,000 - $799,999</option>
<option value="800K-900K">$800,000 - $899,999</option>
<option value="900K-1000K">$900,000 - $999,999</option>
<option value="1000K-1500K">$1,000,000 - $1,499,999</option>
<option value="1500K-2000K">$1,500,000 - $1,999,999</option>
<option value="2000K-5000K">$2,000,000 - $4,999,999</option>
<option value=">5000K">$5,000,000 or greater</option>
</select>
</p>


<label for="newCoverageDuration" >What duration for coverage?</label>
<p>
<select name="newCoverageDuration" id="newCoverageDuration" required>
<option value="" selected="selected">Please select</option>
<option value="10 years">10 years</option>
<option value="15 years">15 years</option>
<option value="20 years">20 years</option>
<option value="30 years">30 years</option>
<option value="Permanent">Permanent</option>
</select>
</p>

<p>
Have you ever used any tobacco or nicotine products?
<small>(Even if you smoke or recently quit, our agents may be able to find you low rates).</small></p>
<p>
<label>Yes&nbsp;<input type="radio" name="hasUsedTobacco" id= "tobaccoYes" value= "Y" required></label> &nbsp;&nbsp;
<label>No&nbsp;<input type="radio" name="hasUsedTobacco" id= "tobaccoNo" value = "N" required></label>
</p>
<p>
Cigarettes: &nbsp;
<select name = "tobaccoCigarettesState" id="tobaccoCigarettesState">
  <option value="" selected="selected">Please select</option>
  <option value="never">Never</option>
  <option value="I currently smoke">I currently smoke</option>
  <option value="I quit within the last year">I quit within the last year</option>
  <option value="I quit more than a year ago">I quit more than a year ago</option>
  <option value="I quit more than 2 years ago">I quit more than 2 years ago</option>
  <option value="I quit more than 3 years ago">I quit more than 3 years ago</option>
  <option value="I quit more than 4 years ago">I quit more than 4 years ago</option>
  <option value="I quit more than 5 years ago">I quit more than 5 years ago</option>
</select>
</p>
<p>
Other: &nbsp;
<select name="tobaccoOtherState" id="tobaccoOtherState">
  <option value="" selected="selected">Please select</option>
  <option value="never">Never</option>
  <option value="Current user">Current user</option>
  <option value="I quit within the last year">I quit within the last year</option>
  <option value="I quit more than a year ago">I quit more than a year ago</option>
  <option value="I quit more than 2 years ago">I quit more than 2 years ago</option>
  <option value="I quit more than 3 years ago">I quit more than 3 years ago</option>
  <option value="I quit more than 4 years ago">I quit more than 4 years ago</option>
  <option value="I quit more than 5 years ago">I quit more than 5 years ago</option>
</select>
</p> 

<p>
Have you received any driving violations, besides parking tickets, in the past five years?
</p>
<p>
  <label>Yes&nbsp;<input type="radio" name="drivingViolations" id= "drivingViolationYes" value= "Y" required></label> &nbsp;&nbsp;
  <label>No&nbsp;<input type="radio" name="drivingViolations" id= "drivingViolationNo" value = "N" required></label>
</p>
<p>
  <label># of violations in the past 5 years: &nbsp;
  <select name="violationsPast5Years" id="violationsPast5Years">
    <option value="" selected="selected">Please select</option>
    <option value="1">1</option>
    <option value="2">2</option>
    <option value="3">3</option>
    <option value="4">4</option>
    <option value="5">5</option>
    <option value="6 or more">6 or more</option>
  </select></label>
</p>
<p>
  <label># of violations in the past 3 years: &nbsp;
  <select name="violationsPast3Years" id="violationsPast3Years">
    <option value="" selected="selected">Please select</option>
    <option value="1">1</option>
    <option value="2">2</option>
    <option value="3">3</option>
    <option value="4">4</option>
    <option value="5">5</option>
    <option value="6 or more">6 or more</option>
  </select></label>
</p>
<p>
Do you currently engage in <em>any</em> of these activities or sports?
</p>
<table>
  <tr>
  <td>
    <ul>
    <li>Piloting aircraft</li>
    <li>Hang gliding</li>
    <li>Bungee jumping</li>
    </ul>
  </td>
  <td>
    <ul>
    <li>Scuba diving</li>
    <li>Mountain & rock climbing</li>
    <li>Skydiving</li>
    </ul>
  </td>
</tr>
</table>
 
<label> Yes
<input type="radio" name="engagesInDangerousActivities" id= "dangerousActivitiesYes" value= "Y" required></label> &nbsp;&nbsp;
<label> No
<input type="radio" name="engagesInDangerousActivities" id= "dangerousActivitiesNo" value = "N" required></label>


<p>Have you ever been treated for any of the below? (Check all that apply.) <small>Even if you have less than perfect health, our agents may be able to find you affordable rates. </small></p>

<table border="0">
  <tr><td>
<label><input name="alcohol" type="checkbox" id="Alcohol" value="Y" > Alcohol or Substance Abuse</label><br>
<label><input name="cholesterol" type="checkbox" id="Cholesterol" value="Y" > Cholesterol</label><br>
<label><input name="asthma" type="checkbox" id="Asthma" value="Y" > Asthma</label><br>
<label><input name="depressionAnxiety" type="checkbox" id="DepressionAnxiety" value="Y" > Depression or Anxiety</label><br>
<label><input name="bloodPressure" type="checkbox" id="Bloodpressure" value="Y"> Blood Pressure</label><br>
</td>
<td>
<label><input name="diabetes" type="checkbox" id="Diabetes" value="Y" > Diabetes</label><br>
<label><input name="cancer" type="checkbox" id="Cancer" value="Y" > Cancer</label><br>
<label><input name="heartIssue" type="checkbox" id="Heartissue" value="Y" > Heart issue</label><br>
<label><input name="sleepApnea" type="checkbox" id="Sleepapnea" value="Y" > Sleep Apnea</label><br>
<label><input name="noneOfThese" type="checkbox" id="Noneofthese" value="Y"> None of these</label><br>
</td></tr>
</table>

<p>
Did your parents and/or siblings have any incidents of heart disease, cancer, stroke or diabetes before the age of 65?
</p>
<p>
<label>Yes&nbsp;<input type="radio" name="familyRisk" id= "familyriskYes" value= "Y" required></label> &nbsp;&nbsp;
<label>No&nbsp;<input type="radio" name="familyRisk" id= "familyriskNo" value = "N" required></label>
</p>


<table>
<tr>
  <td>

<label for="inputFirst">First Name </label>
<input type="text" name="firstname" id="inputFirst" placeholder="" required>
  </td>
  <td>

<label for="inputLast">Last Name</label>
<input type="text" name="lastname" id="inputLast" placeholder="" required>

  </td></tr>
<tr>
  <td>

<label for="streetAddress">Street Address </label>
<input type="text" name="streetAddress" id="streetAddress" placeholder="" required>

<td>

<label for="city">City </label>
<input type="text" name="city" id="city" placeholder="" required>

</tr>
<tr>
  <td>

<label for="state">State </label>
<input type="text" name="state" id="state" placeholder="" pattern="[A-Za-z]{2}" title="Please enter two-letter state abbreviation" maxlength="2" required>

<td>

<label for="zipcode">Zip </label>
<input type="text" name="zipcode" id="zipcode"  pattern="[0-9]{5}" placeholder="" minlength="5" maxlength="5" title="Zip code must contain 5 digits" required>

</tr>
<tr>
  <td>

<label for="inputEmail">Email </label>
<input type="email" name="email" id="inputEmail" placeholder="" required>

 <td>

<label for="phone">Phone </label>
<input type="phone" name="phone" id="phone" placeholder="" pattern="[0-9]{10}" title="Phone # must have 10 digits" maxlength="10" required>

</tr>
<tr>
<td colspan="2">

<label for="inputComments" >Any Additional Comments: </label><br>
<textarea name="additionalComments" id="inputComments" rows="5" placeholder="(optional)"></textarea>

</tr>
</table>

<p></p>

Please verify everything above one last time, and then submit below!
<p></p>
<button type="submit">Send</button>
</form>

Again, nothing fancy above.

You may have noticed that we have several questions that should only appear when a user chooses a particular answer to a previous question.

For example, the field Current policy coverage amount ($US): should only appear as a question if the user answers Yes to the previous question (Do you currently have a life insurance policy?). Right now that’s not happening because we haven’t introduced any Javascript to our form yet.

Let’s do that next.

2. Add initial Javascript to make certain fields appear or disappear

The key technique we’ll use to add the functionality of showing or hiding certain questions is essentially an event listener that changes the display type of the dependent section’s <div>.

We’ll need to add this event listener to both possible answers to the independent question, such that the subsequent dependent section becomes visible if the user selects “Yes”, and becomes invisible if the user selects “No”.

Let’s first wrap every dependent section around a div, and give each div a unique id.

While we’re at it, we’ll also want to set the initial display type of those divs to be invisible. For the purposes of simplifying this tutorial, we’ll use the inline styling method (i.e. adding the following attribute to each div style="display: none"), but of course the proper way to do this in real life would be to create something like a .invisible class in our separate CSS file, and add that class as an attribute on our div.

There are three dependent sections in our particular form we need to wrap around a unique div and to render initially invisible:

  1. Current Policy coverage amount
  2. Cigarettes & Other
  3. Violations in past 5 years & Violations in past 3 years

The first unique div section above would like this:

<div id="hasInsurance" style="display: none">
<p>
<label for="currentPolicyAmount">Current policy coverage amount ($US): &nbsp;</label>
<input type="number" name="currentPolicyAmount" id="currentPolicyAmount" placeholder="" min=1 >
</p>
</div>

The second unique div section would like this:

<div id="smoker" style="display: none">
<p>
Cigarettes: &nbsp;
<select name = "tobaccoCigarettesState" id="tobaccoCigarettesState">
  <option value="" selected="selected">Please select</option>
  <option value="never">Never</option>
  <option value="I currently smoke">I currently smoke</option>
  <option value="I quit within the last year">I quit within the last year</option>
  <option value="I quit more than a year ago">I quit more than a year ago</option>
  <option value="I quit more than 2 years ago">I quit more than 2 years ago</option>
  <option value="I quit more than 3 years ago">I quit more than 3 years ago</option>
  <option value="I quit more than 4 years ago">I quit more than 4 years ago</option>
  <option value="I quit more than 5 years ago">I quit more than 5 years ago</option>
</select>
</p>
<p>
Other: &nbsp;
<select name="tobaccoOtherState" id="tobaccoOtherState">
  <option value="" selected="selected">Please select</option>
  <option value="never">Never</option>
  <option value="Current user">Current user</option>
  <option value="I quit within the last year">I quit within the last year</option>
  <option value="I quit more than a year ago">I quit more than a year ago</option>
  <option value="I quit more than 2 years ago">I quit more than 2 years ago</option>
  <option value="I quit more than 3 years ago">I quit more than 3 years ago</option>
  <option value="I quit more than 4 years ago">I quit more than 4 years ago</option>
  <option value="I quit more than 5 years ago">I quit more than 5 years ago</option>
</select>
</p> 
</div>

You get the idea, so no need to cover the third (driving violations).

Now that we have all the dependent sections nicely wrapped in their own unique, identifiable div, we can add some event listeners that cause each appropriate div section to become either visible or invisible.

We’ll add the <script></script> tags as the last thing inside our <body></body> tags, so that our Javascript can properly access the already-loaded DOM elements, and add some JS inside those script tags.

We’ll need to do a couple things in our JS:

  1. Create a reusable function that takes two string parameters: a div to show and a div to hide,
  2. Select each appropriate DOM element and add an on-click event listener that triggers the function above with the appropriate two arguments

Here’s what our function might look like. We will not always have a div to hide, so we want to set its default state to false:

function showHide(show, hide=false){
  document.getElementById(show).style.display="block";
  if (hide) document.getElementById(hide).style.display="none";
}

Note that any non-empty string evaluates to truthy in Javascript, which allows us to trigger the if block above when we do pass a second argument into the showHide function, and to skip it when we don’t.

Next, we’ll need to create situations that call this function by adding some event listeners. We want to first identify all the DOM elements that should trigger a change, and then add a tailored version of that function to each, all still inside our <script></script> tags:

//1. Identify all 6 elements that trigger the visible/invisible states of our 3 dependent sections
let insuranceYes = document.getElementById(insuranceYes); // should render hasInsurance visible when clicked
let insuranceNo = document.getElementById(insuranceNo); // should render hasInsurance invisible when clicked

let tobaccoYes = document.getElementById(tobaccoYes); // should render smoker visible when clicked
let tobaccoNo = document.getElementById(tobaccoNo); // should render smoker invisible when clicked

  // same corresponding two lines for our third section (drivingViolations)

//2. Add event listeners to all 6
insuranceYes.addEventListener('click',()=>{showHide('hasInsurance')});
insuranceNo.addEventListener('click',()=>{showHide('empty','hasInsurance')});

tobaccoYes.addEventListener('click',()=>{showHide('smoker')});
tobaccoNo.addEventListener('click',()=>{showHide('empty','smoker')});

  // same corresponding two lines for our third section (drivingViolations)

Several things to point out above the above:

  1. Note that we could have created two separate functions: one to show a div, and the other to hide a div. That would have allowed us to only call the appropriate one depending on the element clicked, instead of using the same function on all elements. However, we did it this way for reasons that will become apparent in sections 3 and 4 of this tutorial.
  2. Notice that when we are hiding the appropriate div, we’re also showing a div called ‘empty’. This is a div at the top of our form, created only for this purpose. It basically does nothing on its own.
  3. Notice that we’re wrapping a call to each customized function (with its appropriate arguments) inside another nameless function. If we only passed the called function directly, JS would trigger that call immediately when loading.

Now that we have some experience hiding and showing divs, the following section should make perfect sense.

3. Carve up our form into different pages using divs

To move through the various stages of the multi-step form, all we’re doing is essentially hiding the current section we just finished, and showing the next session.

To be able to do that, we’ll need to first wrap every page or section into its own div. To make things easier, let’s use the same naming convention for our divs–something like page1, page2, etc.

Also, while we’re doing this, the same way we set the initial state of the dependent sections above to be invisible, let’s also set all the initial divs after page1 to be invisible.

For example, our fields from page 2 would be wrapped inside the following div pair:

<div id="page2" style="display: none">
</div>

We’ll also want to wrap our submit <button> at the end in its own div (called ‘final’), also initially hidden.

Now, the only thing that should appear are the page1 fields. How can we add functionality to get to page2?

We need to add a button as the last element inside our page1 div, which, when triggered, hides page1 and shows page2. Wait–don’t we already have such a function? Yes, as a matter of fact, we do! You guessed it, we can use the same function we created to show and hide our conditional / dependent fields.

Our html to place the button inside our page1 div would look something like this:

<button type="button" id="p1to2">Next</button>

Do the same thing for every page. In addition, also add a ‘Back’ button to every page after the first page. The Back button for page 2 might have an id of “p2to1”, for example, since it takes the user from page 2 back to page 1.

It’s up to you whether you want the user to be able to submit the form from the last page (in which case show the Submit button instead of the Next button), or whether you want the user to be able to review all the fields one last time before submitting.

Great! Now we’ve got some buttons, but they still don’t actually do anything. In the next section, we’ll add some functionality to all of our procedural/transitional buttons.

4. Add more Javascript to power the multi-step functionality

You probably already know exactly what you need to do at this point. I’ll give you just a teaser–the bit of the JS you’ll need to power the first step of the transition (i.e. going from page 1 to page 2).

Of course, this assumes we’ve properly created the ‘page1’ div around our page 1 fields, and the proper ‘page2’ div around our page 2 fields.

let page1next = document.getElementById('p1to2');
page1next.addEventListener('click',()=>{showHide('page2','page1')}); //show page 2 and hide page 1

The rest is basically just some iteration off the above. How different would the code for our Back button functionality be?

Leave a comment below if you have any questions, are struggling with something, or just want to show some appreciation!

5. Add some Bootstrap CSS magic

Our form now has some functionality, but it looks like… Let’s put it this way: would you go on a date with it if you just saw a picture of what it currently looks like?

This is where CSS, and a very useful CSS+JS library like Bootstrap, comes in handy.

Go check out how to include it–it’s just one line for the CSS library, and another line for the JS library. You shouldn’t need the Popper nor the jQuery JS libraries for this purpose.

Some useful classes we’ll use in our form include form-control, which we’ll add to all of our fields. We’ll also use the primary and secondary classes for our Next and Back buttons, respectively.

Finally, this html snippet is what we’ll need for displaying a useful progress bar.

<div class="progress" style="height: 10px;">
  <div class="progress-bar" role="progressbar" style="width: 10%" aria-valuenow="10" aria-valuemin="0" aria-valuemax="100"></div>
</div>

I’ll let you figure out how to display different progress percentages yourself.

For an example of what the final product should look like, check out: https://vahid.vercel.app/multistep-form/

Play around with all of this, and leave comments below if you have any questions. I hope it’s been useful!

Vahid Dejwakh
Vahid Dejwakh
Founder at Briefcat, LLC & Capstone Participant, Launch School

Vahid writes about interesting ideas at the intersection of data, software, philosophy, psychology, policy, and business. He enjoys coffee and has a palate for spicy and diverse foods.

comments powered by Disqus

Related