Planning the Steps Before Writing Code

Multi-step forms are often discussed in terms of styling tricks, but the harder problem is designing a good flow beyond basic validation. The structure of the form matters as much as its look. When planning, group related questions logically, keep each step focused, and avoid asking for more than necessary at any point.

The example used here is a job application form with four stages: personal information (name, email, phone), work experience (recent company, job title, years), skills and qualifications (skills list, highest degree), and a final review screen that shows entered data before submission. The ordering mirrors a natural conversation — you wouldn't ask about someone's career before learning their name.

In the HTML, each step is a <fieldset>. The first is visible; all others carry the hidden attribute so users only see one section at a time. Navigation buttons are added later to move between steps.

For styling, this tutorial relies on the Simple.css framework to keep the form presentable without writing extensive custom CSS. A separate style.css file adds a few adjustments on top of that base.

Validation at Each Step

A common UX failure in multi-step forms is deferring all validation until the final submit. Users should be told about errors immediately, on the step where they occur, with messages that explain what went wrong and how to fix it.

The first step gets a Next button. The middle steps get both Previous and Next. The final review step ends with Previous and Submit. The buttons carry onclick handlers pointing to previousStep() and nextStep().

Before those functions can work, several variables are needed:

  • References to the input fields to validate
  • An array to accumulate error messages
  • A DOM element (a <div>) below the last fieldset where messages appear
  • A counter, let currentStep = 1;

Moving Forward with nextStep()

The nextStep() function clears any existing error messages first, then calls a validation check for the active step. If errors exist, they are shown; otherwise, the user proceeds to the following step.

Validation is handled by an addValidationErrors() function that takes the fields of the current step as arguments. A separate validateStep() function checks whether the error array is empty. When it is, navigation continues via a showStep() function.

The showStep() function iterates over all fieldsets in the form, hiding any that do not match the target step, then updates the currentStep variable accordingly. Fieldsets are fetched from the DOM and stored in a variable at the top of the script for this purpose.

Going Back with previousStep()

The previousStep() function works similarly to its forward counterpart, clearing error messages and relying on showStep() to navigate. Calling showStep(currentStep - 1) moves the user back one section.

Visual Feedback and Data Persistence

Beyond validation, visual cues improve the experience. A progress stepper, added directly below the opening <form> tag, tells users where they are in the sequence. A span with class currentStep inside that stepper is queried from the DOM. The showStep() function is updated so the stepper text updates whenever the current step changes.

Another major usability issue with long forms is data loss. If a user refreshes the page or accidentally closes the browser, losing all progress is frustrating. Saving data to localStorage as the user types solves that. An input event listener on the form stores each field’s value. On DOMContentLoaded, saved values are written back into the inputs so users can resume from where they stopped.

When the form is finally submitted, stored data should be removed from localStorage to keep the browser clean.

Preserving the Current Step

The same logic applies to position in the form. The current step value is saved to localStorage inside showStep() so even a full browser close doesn't reset the flow. On page load, the saved step is read back and the correct fieldset is shown. The step value is also cleared in the submit handler once the form is sent.

The complete code for this multi-step form is available on GitHub.