Skip to content

Step Indicator

Communicates progress through a sequence of steps, showing which steps are completed, which one is active, and which are still ahead. Useful for multi-step forms, wizards, and onboarding flows.

Usage

Use a step indicator when a task is split into a clear, ordered sequence of steps. Steps are numbered starting from 1. The first item is step 1, the second is step 2, and so on. You keep the number of the currently active step in a variable in your Alpine scope and pass that variable to the component, so it stays in sync with "Next"/"Back" controls. Clicking a step also updates the variable, letting users jump straight to that step. The content of each marker is up to you: a number, an icon, or anything else.

For narrow spaces, set data-collapsed="true" to switch to a compact summary. Only the active step stays visible and the connectors hide, so the footprint stays roughly constant no matter how many steps there are. Add the optional counter and progress parts to show which step you are on. The collapsed layout keeps its constant footprint even with many steps, where a full row would otherwise overflow.

API Reference

Component attribute(s)

x-h-step-indicator
x-h-step-indicator-item
x-h-step-indicator-trigger
x-h-step-indicator-marker
x-h-step-indicator-content
x-h-step-indicator-title
x-h-step-indicator-description
x-h-step-indicator-separator
x-h-step-indicator-counter
x-h-step-indicator-progress

Attributes

x-h-step-indicator

AttributeTypeRequiredDescription
selfnumbertrueA reference to the variable that holds the number of the active step. Clicking a step writes the new number to it.
data-orientationhorizontal
vertical
falseLayout direction. Defaults to horizontal. Reactive, so you can bind it to switch orientation at runtime.
data-collapsedtrue
false
falseWhen true, switches to the compact summary. Only the active step stays visible, connectors hide, and the counter and progress parts appear. Reactive, so you can bind it. Works in both orientations.

x-h-step-indicator-item

AttributeTypeRequiredDescription
selfnumbertrueThe number of this step. The first step is 1, the next 2, and so on.

Each item exposes its state through a data-state attribute of inactive, active, or completed, which the child parts use for their styling.

x-h-step-indicator-trigger

AttributeTypeRequiredDescription
data-non-interactivebooleanfalseWhen true, clicking this step does not change the active step. A common use is to let users return to steps they have already passed while preventing them from skipping ahead to steps they have not reached yet.

x-h-step-indicator-counter

Place inside the step indicator to show a "Step X of Y" label. It is only visible when the indicator is collapsed. The total is counted from the items automatically, so it works with x-for.

AttributeTypeRequiredDescription
data-step-labelstringfalseWord before the current step number. Defaults to Step.
data-of-labelstringfalseWord between the current step and the total. Defaults to of.

x-h-step-indicator-progress

Place inside the step indicator to show a thin progress bar that fills to the active step over the total. It is only visible when the indicator is collapsed and needs no attributes.

Examples

Default

html
<div x-data="{ step: 2, total: 3 }" class="vbox w-full gap-6">
  <div x-h-step-indicator="step">
    <div x-h-step-indicator-item="1">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>
          <svg x-h-icon data-icon="circle-user" role="img" aria-label="step account"></svg>
        </span>
        <span x-h-step-indicator-content>
          <span x-h-step-indicator-title>Account</span>
          <span x-h-step-indicator-description>Your details</span>
        </span>
      </button>
      <div x-h-step-indicator-separator></div>
    </div>
    <div x-h-step-indicator-item="2">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>
          <svg x-h-lucide role="img" aria-label="step address" data-lucide="map-pin"></svg>
        </span>
        <span x-h-step-indicator-content>
          <span x-h-step-indicator-title>Address</span>
          <span x-h-step-indicator-description>Shipping info</span>
        </span>
      </button>
      <div x-h-step-indicator-separator></div>
    </div>
    <div x-h-step-indicator-item="3">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>
          <svg x-h-lucide role="img" data-lucide="banknote" aria-label="step payment"></svg>
        </span>
        <span x-h-step-indicator-content>
          <span x-h-step-indicator-title>Payment</span>
          <span x-h-step-indicator-description>Confirm order</span>
        </span>
      </button>
    </div>
  </div>
  <div class="hbox justify-between gap-2">
    <button x-h-button data-variant="outline" @click="step = Math.max(step - 1, 1)" :disabled="step === 1">Back</button>
    <button x-h-button data-variant="primary" @click="step = Math.min(step + 1, total)" :disabled="step === total">Next</button>
  </div>
</div>

Collapsed

Set data-collapsed="true" for a compact summary that keeps a constant footprint no matter how many steps there are. Only the active step stays visible, and the optional counter and progress parts show which step you are on. Bind data-collapsed to switch between the two layouts, as the toggle below does.

html
<div x-data="{ step: 3, total: 5, collapsed: true }" class="vbox w-full gap-6">
  <div x-h-step-indicator="step" :data-collapsed="collapsed">
    <div x-h-step-indicator-item="1">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>1</span>
        <span x-h-step-indicator-content>
          <span x-h-step-indicator-title>Account</span>
          <span x-h-step-indicator-description>Your details</span>
        </span>
      </button>
      <div x-h-step-indicator-separator></div>
    </div>
    <div x-h-step-indicator-item="2">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>2</span>
        <span x-h-step-indicator-content>
          <span x-h-step-indicator-title>Address</span>
          <span x-h-step-indicator-description>Shipping info</span>
        </span>
      </button>
      <div x-h-step-indicator-separator></div>
    </div>
    <div x-h-step-indicator-item="3">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>3</span>
        <span x-h-step-indicator-content>
          <span x-h-step-indicator-title>Payment</span>
          <span x-h-step-indicator-description>Confirm order</span>
        </span>
      </button>
      <div x-h-step-indicator-separator></div>
    </div>
    <div x-h-step-indicator-item="4">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>4</span>
        <span x-h-step-indicator-content>
          <span x-h-step-indicator-title>Review</span>
          <span x-h-step-indicator-description>Check details</span>
        </span>
      </button>
      <div x-h-step-indicator-separator></div>
    </div>
    <div x-h-step-indicator-item="5">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>5</span>
        <span x-h-step-indicator-content>
          <span x-h-step-indicator-title>Done</span>
          <span x-h-step-indicator-description>All set</span>
        </span>
      </button>
    </div>
    <div x-h-step-indicator-counter></div>
    <div x-h-step-indicator-progress></div>
  </div>
  <div class="hbox justify-between gap-2">
    <button x-h-button data-variant="outline" @click="step = Math.max(step - 1, 1)" :disabled="step === 1">Back</button>
    <button x-h-button data-variant="transparent" @click="collapsed = !collapsed" x-text="collapsed ? 'Expand' : 'Collapse'"></button>
    <button x-h-button data-variant="primary" @click="step = Math.min(step + 1, total)" :disabled="step === total">Next</button>
  </div>
</div>

Markers only

html
<div x-data="{ step: 2 }">
  <div x-h-step-indicator="step">
    <div x-h-step-indicator-item="1">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>1</span>
      </button>
      <div x-h-step-indicator-separator></div>
    </div>
    <div x-h-step-indicator-item="2">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>2</span>
      </button>
      <div x-h-step-indicator-separator></div>
    </div>
    <div x-h-step-indicator-item="3">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>3</span>
      </button>
      <div x-h-step-indicator-separator></div>
    </div>
    <div x-h-step-indicator-item="4">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>4</span>
      </button>
    </div>
  </div>
</div>

Generated with x-for

Because each item's step is an expression, you can generate the steps from an array with x-for, deriving the step number from the loop index.

html
<div x-data="{ step: 2, steps: ['Account', 'Address', 'Payment'] }" class="vbox w-full gap-6">
  <div x-h-step-indicator="step">
    <template x-for="(label, i) in steps" :key="i">
      <div x-h-step-indicator-item="i + 1">
        <button x-h-step-indicator-trigger>
          <span x-h-step-indicator-marker x-text="i + 1"></span>
          <span x-h-step-indicator-content>
            <span x-h-step-indicator-title x-text="label"></span>
          </span>
        </button>
        <div x-h-step-indicator-separator x-show="i < steps.length - 1"></div>
      </div>
    </template>
  </div>
  <div class="hbox justify-between gap-2">
    <button x-h-button data-variant="outline" @click="step = Math.max(step - 1, 1)" :disabled="step === 1">Back</button>
    <button x-h-button data-variant="primary" @click="step = Math.min(step + 1, steps.length)" :disabled="step === steps.length">Next</button>
  </div>
</div>

Vertical

html
<div x-data="{ step: 2 }">
  <div x-h-step-indicator="step" data-orientation="vertical">
    <div x-h-step-indicator-item="1">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>1</span>
        <span x-h-step-indicator-content>
          <span x-h-step-indicator-title>Account</span>
          <span x-h-step-indicator-description>Your details</span>
        </span>
      </button>
      <div x-h-step-indicator-separator></div>
    </div>
    <div x-h-step-indicator-item="2">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>2</span>
        <span x-h-step-indicator-content>
          <span x-h-step-indicator-title>Address</span>
          <span x-h-step-indicator-description>Shipping info</span>
        </span>
      </button>
      <div x-h-step-indicator-separator></div>
    </div>
    <div x-h-step-indicator-item="3">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>3</span>
        <span x-h-step-indicator-content>
          <span x-h-step-indicator-title>Payment</span>
          <span x-h-step-indicator-description>Confirm order</span>
        </span>
      </button>
    </div>
  </div>
</div>

Disabled trigger

html
<div x-data="{ step: 2 }">
  <div x-h-step-indicator="step">
    <div x-h-step-indicator-item="1">
      <button x-h-step-indicator-trigger disabled>
        <span x-h-step-indicator-marker>1</span>
        <span x-h-step-indicator-content>
          <span x-h-step-indicator-title>Account</span>
          <span x-h-step-indicator-description>Your details</span>
        </span>
      </button>
      <div x-h-step-indicator-separator></div>
    </div>
    <div x-h-step-indicator-item="2">
      <button x-h-step-indicator-trigger>
        <span x-h-step-indicator-marker>2</span>
        <span x-h-step-indicator-content>
          <span x-h-step-indicator-title>Address</span>
          <span x-h-step-indicator-description>Shipping info</span>
        </span>
      </button>
      <div x-h-step-indicator-separator></div>
    </div>
  </div>
</div>