Building a Form Wizard Step Indicator
AI generated
</>
tw
Tailwind CSS · UI Components · Utility First · Design Patterns
Building a Form Wizard Step Indicator
Orientation for multi-step forms

A good step indicator always shows where you are inside a form wizard, which steps are already done and how many are still to come. With Tailwind CSS and Alpine.js this indicator can be built consistently from a few reusable states, instead of maintaining separate markup for every step.

17 min read Step indicator · Alpine.js · Validation · Accessibility Tailwind CSS v4 · all modern browsers

1. Why a step indicator carries the wizard

A multi-step form without a visible step indicator feels like flying blind for users. Without the information of how many steps are still ahead, a form wizard quickly gives the impression it could go on forever, which measurably raises abandonment rates. A well designed step indicator solves exactly this problem by making progress visible before a single input has been made.

Technically, a step indicator is a pure state machine with a visual representation, each step is either in the done, active or pending state. These three states can be mapped with Tailwind CSS through conditional classes, while Alpine.js handles the actual navigation logic. The following sections build a complete step indicator for a form wizard, from the base structure to full accessibility.

2. Base structure: HTML for the step indicator

The base structure of a step indicator consists of an ordered list, where each list item contains a circle with a number or icon plus a label. An ol list is semantically more fitting than div elements, since it carries the inherent order of the steps even without additional ARIA. Between the circles sits a connector line that highlights the progress already covered in an accent color.

For a form wizard with four steps a simple grid or flex layout with even distribution is enough. It matters that every step inside the step indicator is marked as an independent, clickable element, provided jumping back to already completed steps should be allowed. Future, not yet reached steps should instead not be clickable, so users cannot jump ahead to steps whose prerequisites are still missing.


<!-- Base structure for a four-step wizard indicator -->
<ol class="flex items-center w-full mb-10">
  <li class="flex w-full items-center text-sky-600 after:content-[''] after:w-full after:h-0.5 after:border-b after:border-sky-600 after:border-4 after:inline-block">
    <span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-sky-600 text-white font-bold">
      <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/></svg>
    </span>
  </li>
  <li class="flex w-full items-center text-sky-600 after:content-[''] after:w-full after:h-0.5 after:border-b after:border-slate-200 after:border-4 after:inline-block">
    <span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full border-2 border-sky-600 bg-white text-sky-600 font-bold">2</span>
  </li>
  <li class="flex w-full items-center text-slate-400 after:content-[''] after:w-full after:h-0.5 after:border-b after:border-slate-200 after:border-4 after:inline-block">
    <span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full border-2 border-slate-200 bg-white text-slate-400 font-bold">3</span>
  </li>
  <li class="flex items-center text-slate-400">
    <span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full border-2 border-slate-200 bg-white text-slate-400 font-bold">4</span>
  </li>
</ol>

3. Styling states: done, active, future

Every state in the step indicator needs a clearly distinguishable visual language. The done state uses a filled circle in the accent color and a checkmark icon instead of a number, which unmistakably signals completion. The active state uses the same color tone, but as a border instead of a fill, combined with a bolder font for the label underneath. Future steps stay in neutral gray, to clearly separate them from the already relevant steps.

A common design mistake is too little contrast between the active and future state, which makes the step indicator unreadable at a glance. The combination of color, fill and icon change ensures the state stays recognizable even for users with limited color vision, since color alone should never be the only distinguishing feature.

4. Connector lines between steps

The connector line between two circles in the step indicator carries an additional piece of information, namely whether the transition between two steps has already been fully covered. With the Tailwind pseudo element utility after:, this line can be attached directly to the li element without extra markup, which keeps the structure lean.

For a clean transition, the line color should match exactly the state of the previous step, not the following one. A line between a done step and an active step in the step indicator should therefore appear fully in the accent color, while a line between an active and a future step stays neutral gray. This detail makes the difference between a technically correct and a truly thought through indicator.


/* Connector line styling via Tailwind after: pseudo-element utilities */
.step-connector {
  /* Applied conditionally based on step state */
}

/* Completed connector: full accent color */
.step-connector--done::after {
  border-color: theme(colors.sky.600);
}

/* Pending connector: neutral gray */
.step-connector--pending::after {
  border-color: theme(colors.slate.200);
}

/* Equivalent Tailwind classes applied conditionally with Alpine :class bindings:
   after:border-sky-600   -> completed transition
   after:border-slate-200 -> pending transition */

5. Alpine.js state for wizard navigation

The actual logic behind a step indicator is surprisingly compact. A single currentStep value in an Alpine.js component is enough to compute all three states for every step, without each step managing its own state separately. A step counts as done when its number is smaller than currentStep, as active when both are equal, and as future in every other case.

This central state management makes the step indicator trivially extensible. A fifth or sixth step simply means one more list item with the same conditional class logic, no additional JavaScript logic needed. For jumping back to already completed steps, a click handler that sets currentStep directly to the target number, provided it is smaller than the current value, is enough.


<!-- Alpine.js component computing step state from a single currentStep value -->
<div x-data="{
  currentStep: 2,
  totalSteps: 4,
  stepState(n) {
    if (n < this.currentStep) return 'done';
    if (n === this.currentStep) return 'active';
    return 'pending';
  }
}">
  <ol class="flex items-center w-full mb-10">
    <template x-for="n in totalSteps" :key="n">
      <li
        class="flex w-full items-center"
        :class="{
          'text-sky-600': stepState(n) !== 'pending',
          'text-slate-400': stepState(n) === 'pending'
        }"
      >
        <button
          type="button"
          @click="if (stepState(n) === 'done') currentStep = n"
          class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full font-bold"
          :class="{
            'bg-sky-600 text-white': stepState(n) === 'done',
            'border-2 border-sky-600 bg-white text-sky-600': stepState(n) === 'active',
            'border-2 border-slate-200 bg-white text-slate-400': stepState(n) === 'pending'
          }"
          x-text="stepState(n) === 'done' ? '' : n"
        ></button>
      </li>
    </template>
  </ol>
</div>

6. Validating each step before moving on

A step indicator that allows moving forward without checking the current step's inputs quickly leads to incomplete forms. The clean solution is a validation function per step, called before incrementing currentStep. If validation fails, the wizard stays on the current step and shows an error message right at the affected field.

For the form wizard this means: the next button does not call currentStep++ directly, but a method goNext() that first checks validateStep(currentStep). Only on successful validation does the step indicator actually advance. This separation of navigation and validation keeps the component testable, since both aspects can be checked independently of each other.

7. Responsive adaptation: mobile vs. desktop

On narrow screens a horizontal step indicator with full text labels quickly becomes cramped, especially with five or more steps. A proven solution is hiding the labels entirely below sm: and showing only the numbered circles with the current step position as text above, for example step 2 of 4. This more compact variant stays readable on every screen without forcing horizontal scrolling.

For very long wizards with six or more steps, a plain progress bar as a mobile alternative to the full step indicator, which stays visible at desktop breakpoints, is worth adding as well. Tailwind's hidden sm:flex combination switches between both representations without maintaining two separate components.


<!-- Mobile: compact progress text, Desktop: full step indicator -->
<div class="sm:hidden mb-6 text-sm font-semibold text-slate-600">
  Step <span x-text="currentStep"></span> of <span x-text="totalSteps"></span>
  <div class="mt-2 h-1.5 w-full rounded-full bg-slate-200">
    <div
      class="h-1.5 rounded-full bg-sky-600 transition-all"
      :style="`width: ${(currentStep / totalSteps) * 100}%`"
    ></div>
  </div>
</div>
<ol class="hidden sm:flex items-center w-full mb-10">
  <!-- Full step indicator markup, hidden below sm: breakpoint -->
</ol>

8. Accessibility: aria-current and screen readers

A purely visually correct step indicator stays meaningless for screen reader users unless ARIA semantics are added. The aria-current="step" attribute on the active list item is the most important building block, it tells screen readers which step is currently active, independent of the visual presentation. Done steps should also be marked as completed through hidden text with an sr-only class, since the checkmark icon alone is not read out.

For the whole step indicator, an enclosing nav element with aria-label="Form progress" is recommended too, so screen reader users can clearly distinguish the navigation from other page content. Clickable steps that lead back to already completed sections should be marked as real button elements, never as plain div elements with a click handler, or native keyboard operability gets lost.

9. Horizontal vs. vertical wizard patterns

Besides the standard horizontal layout, long forms with many detailed steps also use the vertical pattern, where the step indicator sits on the side and content flows below or next to it in a column. Both patterns have specific strengths, best shown in direct comparison.

Criterion Horizontal indicator Vertical indicator
Ideal step count 3 to 5 steps 6 or more steps
Mobile fit Needs a compact variant below sm: Usually converted straight to horizontal
Space requirement Little vertical space Needs a side column, more horizontal space
Label length Short labels preferred Longer description text possible too

For most form wizards with four to five steps, the horizontal variant is the more pragmatic choice, because it needs no extra side column and can be reduced to a compact progress display on mobile devices more easily. The step indicator in vertical form pays off mostly for complex configurators, where each step needs a more detailed description next to the circle icon.

Mironsoft

Tailwind CSS components and design systems

Form wizards that actually get finished?

We design multi-step forms with a clear step indicator, clean per-step validation and full accessibility, to noticeably lower abandonment rates in your checkout and onboarding flows.

UX audit

Reviewing existing wizards for orientation and drop-off points

Component build

Implementing a step indicator with Alpine.js state and validation

Accessibility

aria-current and screen reader testing for the whole wizard

10. Summary

A well thought through step indicator is not a purely decorative element, but a central orientation tool for every form wizard. The three states done, active and future can be made clearly distinguishable with a few Tailwind classes, while a single currentStep variable in Alpine.js handles all state computation. Connector lines between the circles carry additional progress context without needing extra markup.

Validation per step prevents users from jumping ahead with incomplete data, while the responsive switch between the full step indicator and a compact progress bar keeps the component usable on small screens too. aria-current="step" and an enclosing nav element ensure the whole navigation stays fully understandable with a screen reader as well.

Form Wizard Step Indicator — Key Takeaways

States

Distinguish done, active and future clearly through color, fill and icon, not through color alone.

State management

A single central currentStep variable in Alpine.js computes every step state, instead of separate flags.

Validation

Advance only after successful validation of the current step, kept separate from pure navigation.

Accessibility

aria-current="step", sr-only text for done steps, and real button elements for navigation.

11. FAQ: Form Wizard and Step Indicator

1Maximum step count?
Three to five steps ideal, consider vertical or compact display from six onward.
2Allow jumping back to steps?
Yes, only for completed steps. Future steps stay disabled.
3Prevent advancing with incomplete data?
Call a per-step validation function before incrementing currentStep.
4Mobile behavior?
Compact text display with progress bar below the sm: breakpoint.
5Important ARIA attribute?
aria-current="step" on the active list item.
6Is color alone enough?
No, always add icon change and fill difference as extra distinguishing marks.
7Implementing connector lines?
Via the Tailwind after: utility directly on the li element, color follows the predecessor state.
8Vertical instead of horizontal?
From six steps onward, or with longer description text per step.
9How many states are needed?
Three: done, active, future, computed from a single currentStep variable.
10div or button for clickable steps?
Always button, for native keyboard operability without extra logic.