Advertisement
Javascript

Build a Multi-Step Form Interface

by

Form usability is an incredibly important topic in web design. As one of the primary input interfaces provided to users, the usability of a form is essential to a good user experience.

Today, we're going to build a multi-part form, complete with validation and animation. We'll cover a lot of ground, so buckle up!


Form High-level Best Practices

Form interface design is a minefiled of usability obstacles. Before we start, let's talk about a few form best practices.

Don't make your users think too hard

Forms are generally not the place to encourage unique interaction. Good forms provide obvious navigation techniques and full transparency. Good forms are well-labeled and easy to navigate.

Don't get too fancy

It's important not to deviate too far from default form behaviors. There are standards that are the basis for the default behaviors of forms. Deviation from these standards may have negative effects on accesibility, so consider retaining those default styles when possible.

Remember, the user doesn't have to stay

The user of your form doesn't have to stay. They choose to stay. Filling out a form is a chore, so make it easy. (If possible, make it fun!) Don't confuse or demand from the user; instead, establish a dialogue around the questions the form is asking. Be polite.

When to use multi-section techniques

Multi-section forms are certainly a good technique, sometimes. There's not a single answer for "how many inputs does it take before I should split the form up". Instead, always consider whether splitting the form into separate sections will help or hinder usability. Secondarily, consider whether it helps or hinders other aspects of the interaction design.

If you have forms with very distinct sections, it may be worth separating into multiple parts. Checkout processes are a common example of this. (Personal info, shipping info, payment info, and confirmation are very distinct and generally substantial sections.)


Planning the Interaction

We're going to create a signup form with arbitrary fields. We need to know what section we are currently on, so we'll need an indicator at the top of the form. We want to transition our form sections horizontally, sliding from right to left. To accomplish this, we will set the different sections to have absolute positioning inside a section "window" element. We also want to have two buttons; one is the normal submit button. The other is a "next section" button.


The Markup

Here's our form:

<form id="signup" action="somewhere" method="POST">
    <ul id="section-tabs">
      <li class="current active"><span>1.</span> Creds</li>
      <li><span>2.</span> Deets</li>
      <li><span>3.</span> Settings</li>
      <li><span>4.</span> Last Words</li>
    </ul>
  <div id="fieldsets">
  <fieldset class="current">
    <label for="email">Email:</label>
    <input name="email" type="email" class="required email" />
    <label name="password" for="password">Password:</label>
    <input type="password" minlength="10" class="required">
  </fieldset>
  <fieldset class="next">
    <label for="username">Username:</label>
    <input name="username" type="text">
    <label for="bio">Short Bio:</label>
    <textarea name="bio" class="required"></textarea>
  </fieldset>
  <fieldset class="next">
    <label for="interests">Basic Interests:</label>
    <textarea name="bio"></textarea>
    <p>Receive newsletter?<br>
      <input type="radio" name="newsletter" value="yes"><label for="newsletter">yes</label>
      <input type="radio" name="newsletter" value="no"><label for="newsletter">no</label>
    </p>
  </fieldset>
  <fieldset class="next">
    <label for="referrer">Referred by:</label>
    <input type="text" name="referrer">
    <label for="phone">Daytime Phone:</label>
    <input type="tel" name="phone">
  </fieldset>
  <a class="btn" id="next">Next Section ▷</a>
  <input type="submit" class="btn">
  </div>
</form>

The markup is fairly straightforward, but let's talk about a few pieces of it.

  • Fieldsets: Fieldsets are a semantic element for grouping inputs; this suits our situation perfectly.
  • Classnames: jQuery Validate uses classes to define built-in types. We'll see how this works in a minute.
  • The fields are arbitrary. We've wrapped the radio inputs in a paragraph tag for easier formatting.
  • Submit and next buttons: the anchor tag with a class of button will be used to go to the next section. The submit input will show when appropriate via JavaScript.

The Styles (LESS-Flavored)

This is long, get ready..

@import url(http://fonts.googleapis.com/css?family=Merriweather+Sans:300);
@import url(http://fonts.googleapis.com/css?family=Merriweather+Sans:700);


body {
  background: url(http://farm5.staticflickr.com/4139/4825532997\_7a7cd3d640\_b.jpg);
  background-size: cover;
  height: 100%;
  font-family: 'Merriweather Sans', sans-serif;
  color: #666;
}

#signup {
  width: 600px;
  height: auto;
  padding: 20px;
  background: #fff;
  margin: 80px auto;
  position: relative;
  min-height: 300px;
}
#fieldsets {
  position: absolute;
  top: 0px;
  left: 0px;
  width: 100%;
  height: 100%;
  padding: 20px;
  box-sizing: border-box;
  overflow: hidden;
}
input[type=text],
input[type=email],
input[type=password],
input[type=tel],
textarea {
  display: block;
  -webkit-appearance: none;
  -moz-appearance: none;
  width: 100%;
  box-sizing: border-box;
  border: 1px solid #ddd;
  padding: 8px;
  margin-bottom: 8px;
  position: relative;
  &:focus {
    outline: none;
    border: 1px solid darken(#2cbab2,10%);
  }
}

input[type=radio]{
  margin: 6px;
  display: inline-block;
}
fieldset {
  border: none;
  position: absolute;
  left: -640px;
  width: 600px;
  padding: 10px 0;
  transition: all 0.3s linear;
  -webkit-transition: all 0.3s linear;
  -moz-transition: all 0.3s linear;
  -ms-transition: all 0.3s linear;
  opacity: 0;
  &.current {
    left: 20px;
    opacity: 1;
  }
  &.next {
    left: 640px;
  }
}
input[type=submit] {
  display: none;
  border: none;
}
#section-tabs {
  font-size: 0.8em;
  height: 50px;
  position: relative;
  margin-top: -50px;
  margin-bottom: 50px;
  padding: 0;
  font-weight: bold;
  list-style: none;
  text-transform: uppercase;
  li {
    color: #a7a7a7;
    span {
      color: #bababa;
    }
    cursor: not-allowed;
    &.active {
      color: #444;
      cursor: pointer;
    }
    border-left: 1px solid #aaa;
    text-decoration: none;
    padding: 0 6px;
    float: left;
    width: 25%;
    box-sizing: border-box;
    text-align: center;
    font-weight: bold;
    line-height: 30px;
    background: #ddd;
    position: relative;
    &:after {
      content: "";
      display: block;
      margin-left: 0;
      position: absolute;
      left: 0;
      top: 0;
    }
    &.current {
      opacity: 1;
      background: #fff;
      z-index: 999;
      border-left: none;
      &:after {
        border: 15px solid transparent;
        border-left: 15px solid #2cbab2;
      }
    }
  }
}
.error {
  color: #bf2424;
  display: block;
}
input.error, textarea.error {
  border-color: #bf2424;
  &:focus {
    border-color: #bf2424;
  }
}
label.error {
    margin-bottom: 20px;
}
input.valid {
  color: green;
}
label.valid {
  position: absolute;
  right: 20px;
}
input+.valid, textarea+.valid {
  display: none;
}
.valid+.valid {
  display: inline;
  position: absolute;
  right: 10px;
  margin-top: -36px;
  color: green;
}

.btn {
  border: none;
  padding: 8px;
  background: #2cbab2;
  cursor: pointer;
  transition: all 0.3s;
  -webkit-transition: all 0.3s;
  -moz-transition: all 0.3s;
  &:hover {
     background: darken(#2cbab2, 6%);
  }
  color: #fff;
  position: absolute;
  bottom: 20px;
  right: 20px;
  font-family: 'Merriweather Sans', sans-serif;
}

Let's walk through the important part of the styles.

Overview

The form itself is set to a specific width, centered with margin: 0 auto, then set to position:relative. This positioning allows for absolute positioning of child elements to place them absolutely, relative to the containing form. The containing form holds three primary types of elements: the section tabs and the fieldset "window", as well as the buttons.

The section tabs are placed relative to the containing element, and "pulled" up with a negative top margin. We compensate for the effect on the rest of the layout with an equal margin bottom.

The fieldset "window" is set to be positioned absolute, relative to the parent form element. The width and height are both set to 100%. The purpose of this window is to hold elements, then hide them when they fall outside the edges with overflow: hidden. We couldn't do this on the form, because we want to retain the indicator tabs.

Finally, the button elements (an anchor tag and a submit input) are styled to be positioned absolute to the bottom right corner of the form, offset by 20 pixels from the bottom and right. We also have added a simple CSS transition to the button elements to darken the background on hover.

A Few More Notes

  • Fieldsets are set to be position: absolute. We have two classes for two states, and a default state. The default state pulls the fieldset off to the left of the form; the .current class puts the fieldset in the visible area of the form, and finally the .next class pushes the fieldset off to the right of the form.
  • The current class has an opacity of 1; the default (and inherently the .next) states have opacity of 0.
  • We animate the fieldsets between these classes with simple css transitions.
  • The .error and .valid classes will help with the validation styling. We're using Merriweather Sans, a font freely available for use via Google Webfonts.

The JavaScript

Here's the part that makes all of the interaction work. A few notes before we look at the code: this code depends on jQuery and jQuery Validate. jQuery validate is a plugin that has been around for a very long time, and therefore has been tested and proven by thousands of people.

Our basic strategy: set up some validation rules for the form, including a custom function to check for a telephone number. We want users to be able to navigate back through previously finished sections. We want them to be able to use the enter button to move to the next section; however, we don't want to allow users to advance to subsequent sections until previous sections have been completed and are valid.

If the user tries to click on a tab for a section beyond the ones they have completed, we want to avoid navigating to that section.

We want to rely on classes (rather than jQuery animations) to transition between states.

So, with these things in mind, here is the final JavaScript. (Note: if you aren't up on your jQuery, this may be a bit daunting; however, stick with it anyway, and you'll learn by diving into code.) After the full script, we will take it one piece at a time and explain what's going on.

$("#signup").validate({
  success : function(label){
    label.addClass("valid").text("✓");
  },
  error : function(e){
  // do nothing, but register this function
  },
  onsubmit:false,
  rules: {
    phone: {
      required: true,
      phoneUS: true
    }
  }
});

$("body").on("keyup", "form", function(e){
  if (e.which == 13){
    if ($("#next").is(":visible") && $("fieldset.current").find("input, textarea").valid() ){
      e.preventDefault();
      nextSection();
      return false;
    }
  }
});


$("#next").on("click", function(e){
  console.log(e.target);
  nextSection();
});

$("form").on("submit", function(e){
  if ($("#next").is(":visible") || $("fieldset.current").index() < 3){
    e.preventDefault();
  }
});

function goToSection(i){
  $("fieldset:gt("+i+")").removeClass("current").addClass("next");
  $("fieldset:lt("+i+")").removeClass("current");
  $("li").eq(i).addClass("current").siblings().removeClass("current");
  setTimeout(function(){
    $("fieldset").eq(i).removeClass("next").addClass("current active");
      if ($("fieldset.current").index() == 3){
        $("#next").hide();
        $("input[type=submit]").show();
      } else {
        $("#next").show();
        $("input[type=submit]").hide();
      }
  }, 80);

}

function nextSection(){
  var i = $("fieldset.current").index();
  if (i < 3){
    $("li").eq(i+1).addClass("active");
    goToSection(i+1);
  }
}

$("li").on("click", function(e){
  var i = $(this).index();
  if ($(this).hasClass("active")){
    goToSection(i);
  } else {
    alert("Please complete previous sections first.");
  }
});


jQuery.validator.addMethod("phoneUS", function(phone_number, element) {
    phone_number = phone_number.replace(/\s+/g, ""); 
  return this.optional(element) || phone_number.length > 9 &&
    phone_number.match(/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/);
}, "Please specify a valid phone number");

So, let's go piece by piece.

$("#signup").validate({
  success : function(label){
    label.addClass("valid").text("✓");
  },
  error : function(e){
  // do nothing, but register this function
  },
  onsubmit:false,
  rules: {
    phone: {
      required: true,
      phoneUS: true
    }
  }
});

This function is the setup function for jQuery Validate. First, we are telling the plugin to take the signup form and apply validation to it. If validation succeeds, we add a class of valid to the label that the validation plugin inserts after the input element, and replace the text with the utf-8 checkmark .

We are also registering the error callback, although we aren't actually doing anything in this function. Not registering the function seems to have the same callback as the success function on error. We are setting the onsubmit hook to false; this is because pressing enter automatically submits the form, which by default triggers validation to prevent invalid form submission. Preventing the default behavior of form submit doesn't prevent the form validation. The result is that the fields on the "next" screen already show validation errors, despite the form never being submitted.

$("body").on("keyup", "form", function(e){
  if (e.which == 13){
    if ($("#next").is(":visible") && $("fieldset.current").find("input, textarea").valid() ){
      e.preventDefault();
      nextSection();
    }
  }
});

This function listens to the keyup event, triggered on the form. If the key that was hit was enter (key code 13), we then perform the following check. If the next button is still visible and the current section is valid, prevent the default behavior of the enter key being pressed while on a form, which is form submission.

We then call nextSection(), which advances the form to the next section. If the current section contains any invalid inputs, those inputs are identified and the form does not advance. If the next button is not visible, that means we are on the final section (which we will see in subsequent functions) and we want to allow the default behavior (form submission) to occur.

$("#next").on("click", function(e){
  nextSection();
});

$("form").on("submit", function(e){
  if ($("#next").is(":visible") || $("fieldset.current").index() < 3){
    e.preventDefault();
  }
});

These functions are straightforward. If you press the button with the id of "next", we want to advance to the next section. Remember the nextSection() function contains all necessary checks for form validation.

On the submit event on the form, we want to avoid form submission if the next button is visible or if the current fieldset isn't the last one.

function goToSection(i){
  $("fieldset:gt("+i+")").removeClass("current").addClass("next");
  $("fieldset:lt("+i+")").removeClass("current");
  $("li").eq(i).addClass("current").siblings().removeClass("current");
  setTimeout(function(){
    $("fieldset").eq(i).removeClass("next").addClass("current active");
      if ($("fieldset.current").index() == 3){
        $("#next").hide();
        $("input[type=submit]").show();
      } else {
        $("#next").show();
        $("input[type=submit]").hide();
      }
  }, 80);

}

The goToSection() function is the workhorse behind the navigation of this form. It takes a single argument - the index of the target navigation. The function takes all fieldsets with an index greater than the passed in index parameter and removes the current class, and adds a next class. This pushes the elements to the right of the form.

Next, we remove the current class from all fieldsets with an index less than the passed in index. Next, we choose the list item equal to the passed in index and add a current class.

Subsequently we remove the current class from all other sibling lis. We set a timeout, after which we remove the next class and add the current and active classes to the fieldset with an index equal to the passed in index parameter.

The active class lets us know that the user can navigate to this particular section again. If the passed in index parameter is 3 (the final fieldset), we hide the next button and show the submit button. Otherwise, we want to make sure the next button is visible and the submit button is hidden. This allows us to hide the submit button unless the last fieldset is visible.

function nextSection(){
  var i = $("fieldset.current").index();
  if (i < 3){
    $("li").eq(i+1).addClass("active");
    goToSection(i+1);
  }
}

$("li").on("click", function(e){
  var i = $(this).index();
  if ($(this).hasClass("active")){
    goToSection(i);
  } else {
    alert("Please complete previous sections first.");
  }
});

The nextSection() function is basically a wrapper around the goToSection() function. When nextSection is called, a simple check is made to be sure that we are not on the last section. As long as we are not, we go to the section with an index that is equal to the index of the current section, plus one.

We are listening to the click event on the list items. This function checks to make sure the list item has the active class, which it receives once the user initially arrives to that section of the form by completing all previous sections. If the list item does have the class, we call goToSection and pass in the index of that list item. If the user clicks on a list item corresponding to a section they can't access yet, the browser alerts them to let them know that they have to complete the previous sections before advancing.

jQuery.validator.addMethod("phoneUS", function(phone_number, element) {
    phone_number = phone_number.replace(/\s+/g, ""); 
  return this.optional(element) || phone_number.length > 9 &&
    phone_number.match(/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/);
}, "Please specify a valid phone number");

Finally, we are adding a method specified by the jQuery Validate plugin that manually checks an input. We won't spend much time on this, because this exact functionality can be found in the Validate documentation.

Essentially, we check to make sure the text the user put into the phone field matches the regex (the long string of numbers and symbols). If it does, then the input is valid. If it doesn't, the message "Please specify a valid phone number" is added after the input. You could use this same functionality to check for any kind of input (it doesn't have to use a regex).

Note: Do not use this as a password authentication method. It is very unsafe and anyone can view the source to see the password.


Conclusion

We've used semantic HTML and simple LESS combined with some minimal JavaScript to build a robust form interaction. The methods used to build this form, especially using classnames to identify state and to define functionality and animations, can be used in almost any interactive project. The same functionality could be used for demonstration step-throughs, games, and anything else that depends on simple state-based interactions.

What else would you do to this form? What types of interactions have you found to help users more naturally flow through a tedious process such as filling out a long multi-step form? Share them in the comments!

Related Posts
  • Code
    HTML5
    HTML5: Battery Status APIPdl54 preview image@2x
    The number of people browsing the web using mobile devices grows every day. It's therefore important to optimize websites and web applications to accommodate mobile visitors. The W3C (World Wide Web Consortium) is well aware of this trend and has introduced a number of APIs that help with this challenge. In this article, I will introduce you to one of these APIs, the Battery Status API.Read More…
  • Web Design
    HTML/CSS
    Creating Friendlier, “Conversational” Web FormsForm retina
    Web forms are constantly a hot topic when it comes to web design and user interaction. The reasons for this are vast, but one of the more obvious reasons is that forms are the most basic way for a user to input information into your application. In this article, we'll discuss a few techniques that allow your forms to respond to the user's input, while helping to obscure unnecessarily confusing or overwhelming elements.Read More…
  • Web Design
    UX
    Walk Users Through Your Website With Bootstrap TourTour retina
    When you have a web application which requires some getting used to from your users, a walkthrough of the interface is in order. Creating a walkthrough directly on top of the interface makes things very clear, so that's what we're going to build, using Bootstrap Tour.Read More…
  • Web Design
    UX
    Implementing the Float Label Form PatternForm float input retina
    Using Matt Smith’s mobile form interaction design as a guide, we will create a stunning form interaction for the web that’s both beautiful and accessible using HTML, CSS and JavaScript.Read More…
  • Code
    HTML & CSS
    Mobile First With Bootstrap 3Bootstrap 3 preview
    Ok so a couple of weeks now, on it's very own two year anniversary Mark Otto and the rest of the guys responsible for the develop and maintenance of Bootstrap announced the official release of the framework's third version, and it came on steroids, let's see what we're getting.Read More…
  • Code
    HTML & CSS
    Pure: What, Why, & How?Pure retina preview
    This tutorial will introduce you to Pure, a CSS library made of small modules, that can help you in writing completely responsive layouts, in a very fast and easy way. Along the way, I'll guide you through the creation of a simple page in order to highlight how you can use some of the library's components.Read More…