How to Build a Multi-Step Form in Bootstrap 5

  • Canvas Team
  • 10 min read
How to Build a Multi-Step Form in Bootstrap 5
10 min read
Share:

Bootstrap 5 multi-step form from scratch: markup, navigation logic, client-side validation per step, and a progress indicator — no jQuery dependency required.

Key Takeaways

  • A Bootstrap 5 wizard form uses tab-like panel switching controlled by vanilla JavaScript — no plugin required for the core mechanic.
  • Validate each step individually using the Bootstrap 5 Validation API before advancing, so errors surface at the right moment.
  • A <progress> element or a custom step indicator built with Bootstrap utilities gives users a clear sense of where they are.
  • Accessibility requires correct use of aria-current, aria-disabled, and focus management when steps change.
  • The pattern integrates cleanly into any Bootstrap 5 template — including Canvas HTML Template — without touching core framework files.

When to Use a Multi-Step Form (and When Not To)

A wizard form is not always the right choice. For forms with three fields or fewer, splitting steps adds friction, not clarity. The pattern earns its keep when:

  • You have eight or more distinct fields that can be logically grouped.
  • Different user paths exist — for example, business versus personal details that only apply conditionally.
  • Earlier answers affect which later fields are shown (progressive disclosure).
  • The form represents a meaningful commitment — sign-up, purchase, loan application — where reassuring progress indicators matter psychologically.

If your form is simply long but not logically segmented, consider using anchor links to sections instead. For further reading on form conversion principles, see our post on 7 Bootstrap 5 form designs that improve conversions before deciding on the wizard pattern.

Four blue gradient shapes on a dark background.
Photo by Puscas Adryan on Unsplash

HTML Structure: Steps as Panels

The core concept is a set of <fieldset> panels, only one of which is visible at a time. Bootstrap’s d-none utility handles visibility. Each fieldset represents one logical step.

<div class="multi-step-form" id="wizardForm">

  <!-- Step indicator -->
  <ol class="step-indicator d-flex list-unstyled mb-4">
    <li class="step-item active" data-step="1">1. Account</li>
    <li class="step-item" data-step="2">2. Profile</li>
    <li class="step-item" data-step="3">3. Review</li>
  </ol>

  <form novalidate id="stepForm">

    <!-- Step 1 -->
    <fieldset class="form-step" id="step-1">
      <legend class="h5 mb-3">Account Details</legend>
      <div class="mb-3">
        <label for="email" class="form-label">Email address</label>
        <input type="email" class="form-control" id="email"
               name="email" required>
        <div class="invalid-feedback">Please enter a valid email.</div>
      </div>
      <div class="mb-3">
        <label for="password" class="form-label">Password</label>
        <input type="password" class="form-control" id="password"
               name="password" minlength="8" required>
        <div class="invalid-feedback">Minimum 8 characters.</div>
      </div>
    </fieldset>

    <!-- Step 2 -->
    <fieldset class="form-step d-none" id="step-2">
      <legend class="h5 mb-3">Profile Information</legend>
      <div class="mb-3">
        <label for="fullName" class="form-label">Full name</label>
        <input type="text" class="form-control" id="fullName"
               name="fullName" required>
        <div class="invalid-feedback">Full name is required.</div>
      </div>
      <div class="mb-3">
        <label for="role" class="form-label">Role</label>
        <select class="form-select" id="role" name="role" required>
          <option value="">Choose...</option>
          <option>Developer</option>
          <option>Designer</option>
          <option>Manager</option>
        </select>
        <div class="invalid-feedback">Please select a role.</div>
      </div>
    </fieldset>

    <!-- Step 3: Review -->
    <fieldset class="form-step d-none" id="step-3">
      <legend class="h5 mb-3">Review &amp; Submit</legend>
      <p>Please confirm your details before submitting.</p>
      <dl id="reviewSummary" class="row"></dl>
    </fieldset>

    <!-- Navigation -->
    <div class="d-flex justify-content-between mt-4">
      <button type="button" class="btn btn-outline-secondary"
              id="prevBtn" disabled>Back</button>
      <button type="button" class="btn btn-primary"
              id="nextBtn">Next</button>
      <button type="submit" class="btn btn-success d-none"
              id="submitBtn">Submit</button>
    </div>

  </form>
</div>

Key decisions in this markup: novalidate on the <form> disables native browser validation so Bootstrap’s validation classes drive all feedback. Each <fieldset> uses the semantic <legend> element, which screen readers announce as a group label — this matters for the accessibility work discussed below.

JavaScript: Step Navigation and Per-Step Validation

The navigation script maintains a currentStep integer and validates only the currently visible fieldset’s inputs before moving forward. This prevents users reaching step 3 with invalid step 1 data.

<script>
(function () {
  'use strict';

  const steps    = Array.from(document.querySelectorAll('.form-step'));
  const prevBtn  = document.getElementById('prevBtn');
  const nextBtn  = document.getElementById('nextBtn');
  const submitBtn = document.getElementById('submitBtn');
  const indicators = Array.from(document.querySelectorAll('.step-item'));
  let current = 0;

  function showStep(index) {
    steps.forEach((s, i) => s.classList.toggle('d-none', i !== index));
    indicators.forEach((el, i) => {
      el.classList.toggle('active', i === index);
      el.setAttribute('aria-current', i === index ? 'step' : 'false');
    });
    prevBtn.disabled = index === 0;
    nextBtn.classList.toggle('d-none', index === steps.length - 1);
    submitBtn.classList.toggle('d-none', index !== steps.length - 1);
    if (index === steps.length - 1) buildReview();
    steps[index].querySelector('input, select, textarea')?.focus();
  }

  function validateStep(index) {
    const fields = steps[index].querySelectorAll('input, select, textarea');
    let valid = true;
    fields.forEach(field => {
      if (!field.checkValidity()) {
        field.classList.add('is-invalid');
        valid = false;
      } else {
        field.classList.remove('is-invalid');
        field.classList.add('is-valid');
      }
    });
    return valid;
  }

  function buildReview() {
    const dl = document.getElementById('reviewSummary');
    const form = document.getElementById('stepForm');
    const data = new FormData(form);
    dl.innerHTML = '';
    for (const [key, value] of data.entries()) {
      dl.innerHTML += `<dt class="col-sm-4">${key}</dt>
                       <dd class="col-sm-8">${value || '—'}</dd>`;
    }
  }

  nextBtn.addEventListener('click', () => {
    if (validateStep(current)) {
      current++;
      showStep(current);
    }
  });

  prevBtn.addEventListener('click', () => {
    current--;
    showStep(current);
  });

  document.getElementById('stepForm').addEventListener('submit', e => {
    e.preventDefault();
    alert('Form submitted successfully!');
    // Replace with your fetch/XHR submission logic
  });

  showStep(0);
})();
</script>

The validateStep function uses the native checkValidity() API that Bootstrap 5’s validation layer already leverages internally — no duplication of logic. Notice that is-invalid and is-valid classes map directly to Bootstrap’s built-in validation styles, so no extra CSS is needed for error states.

a man standing in a dark forest with red and green lights
Photo by Elliot Wilkinson on Unsplash

Step Indicator and Progress Bar

A visual indicator is not decorative — it directly reduces abandonment by telling users how far they have come. Two practical options exist in Bootstrap 5:

  1. Custom step indicator (used above): a styled <ol> where each item gets an active class and optional connector lines via CSS pseudo-elements.
  2. Bootstrap <progress> element: simpler to implement, semantically correct, and announced by screen readers.
<!-- Add above the form -->
<div class="mb-3">
  <progress id="formProgress" class="w-100" max="3" value="1"
            aria-label="Form completion progress"></progress>
</div>

Update it in showStep: document.getElementById('formProgress').value = index + 1;

For visual customisation you can override the accent colour using Bootstrap 5’s CSS custom properties — see our overview of SCSS vs CSS variables for theming Bootstrap 5 for the trade-offs between both approaches.

Accessibility Requirements

A wizard form that fails accessibility testing is a form that excludes users and often fails legal compliance checks (WCAG 2.1 AA is legally mandated in many jurisdictions as of 2025). The minimum requirements are:

  • Set aria-current="step" on the active indicator item and "false" on inactive ones — the JavaScript above does this inside showStep.
  • Move keyboard focus to the first focusable element of each new step on transition. The .focus() call in showStep handles this.
  • Ensure <fieldset> + <legend> structure is used so group context is announced. Do not replace these with generic <div> elements.
  • Hidden steps must not be reachable by keyboard. Because d-none sets display: none, those elements are fully removed from the tab order — use it instead of visibility: hidden or opacity: 0.
  • Announce step changes to screen reader users with a visually hidden live region if your audience includes heavy assistive-technology users.

The same discipline applies to navigation patterns across your entire site. Our guide to building an accessible Bootstrap 5 navigation bar covers related ARIA patterns in more depth.

Styling the Step Indicator with CSS

The functional markup above needs minimal CSS to look polished. Below is a self-contained block that works alongside Bootstrap 5 without conflicts:

<style>
.step-indicator { gap: 0; counter-reset: step-counter; }
.step-item {
  flex: 1;
  text-align: center;
  padding: .5rem;
  font-size: .85rem;
  color: #6c757d;
  border-bottom: 3px solid #dee2e6;
  transition: border-color .3s, color .3s;
}
.step-item.active {
  color: var(--bs-primary);
  border-bottom-color: var(--bs-primary);
  font-weight: 600;
}
</style>

If you are working inside a Canvas HTML Template project, replace var(--bs-primary) with var(--cnvs-themecolor) to inherit the template’s global accent colour automatically. This keeps step indicator styling in sync with any theme colour changes without touching the JavaScript.

Common Mistakes to Avoid

  • Validating the entire form on “Next” instead of only the current step. Users should not see errors for fields they have not reached yet.
  • Using visibility: hidden or opacity: 0 to hide inactive steps. Hidden inputs remain in the tab order and get included in FormData, which can cause unexpected validation failures.
  • Not persisting field values between steps. If the user navigates back and forward, inputs must retain their values. Native form elements do this automatically as long as you do not destroy and recreate the DOM.
  • Omitting a way to go back. The Back button is a trust signal — remove it and abandonment rises significantly.
  • Putting a submit button on non-final steps. It is easy to accidentally submit an incomplete form. Keep the submit button hidden until the final step, as the markup above demonstrates.

Frequently Asked Questions

No. The implementation described here uses only the Bootstrap 5 utility classes for visibility (d-none), its built-in validation CSS, and about 50 lines of vanilla JavaScript. jQuery is not required and has not been a Bootstrap dependency since Bootstrap 5.0.0 released in May 2021.

Store an array of step indices to display and build the sequence dynamically based on a value collected in an earlier step. Filter the steps array before the navigation logic runs, or push and splice step indices at runtime. The core showStep(index) function does not need to change — only the array it iterates changes.

Yes. Serialise new FormData(form) to a plain object and write it to localStorage after each step advance. On page load, read it back and populate the fields before calling showStep(0). For sensitive data — such as financial forms — prefer a server-side session or short-lived signed token over localStorage.

Research from Formisimo and Baymard Institute consistently shows abandonment rises sharply beyond five steps for general audiences. If your flow genuinely requires more, group related steps into larger chunks rather than increasing the step count. For complex onboarding — such as a fintech application with regulatory questions — six to eight steps can be justified when each step is clearly labelled and a progress indicator is visible throughout.

Yes. The pattern uses only Bootstrap utility classes and standard HTML form elements. It drops into any Bootstrap 5 template without modifying framework files. In Canvas HTML Template specifically, you can use var(--cnvs-themecolor) in your step indicator CSS to inherit the active theme colour, and the form’s validation states will adopt Canvas’s typography and spacing automatically.

Looking for a production-ready Bootstrap 5 HTML template? Browse Canvas Template demos and find the perfect starting point for your next project.

If you’re building with the Canvas HTML Template and want to ship production-ready Bootstrap 5 layouts faster, try Canvas Builder free — the visual builder that exports clean Canvas-ready markup in minutes.

Skip the setup — build it free

Spin up a complete Bootstrap 5 site, blog included, with Canvas Builder. No coding, no cost.

Share:
Canvas Team
Canvas Team

Tutorials and tips for building beautiful Bootstrap 5 websites with the Canvas HTML Template and Canvas Builder.

More from the Canvas Blog