Stepper — <mv-stepper>

Horizontal or vertical steps (completed, current, upcoming) with connectors that fill with the accent color, automatic numbers, an animated checkmark and a loading step; optional <mv-stepper value> to drive the states.

CategoryNavigation
TypeWeb Component (<mv-stepper>)
Statusstable
Keywordsstepper, steps, étapes, wizard, multi-step, progress, timeline, checkout

When to use

Avoid when

Install

node scripts/add.mjs stepper --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["stepper"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, components/stepper/stepper.css, components/stepper/stepper.js.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div style="display:grid;gap:2.5rem;width:100%;max-width:46rem;margin-inline:auto">
  <!-- Horizontal, controlled, clickable (linear) -->
  <div style="display:grid;gap:1.25rem">
    <mv-stepper id="st-demo-checkout" value="2" linear>
      <ol class="mv-stepper" aria-label="Checkout steps">
        <li class="mv-stepper-item"><button class="mv-stepper-trigger"><span class="mv-stepper-indicator"></span><span class="mv-stepper-text"><span class="mv-stepper-title">Cart</span></span></button></li>
        <li class="mv-stepper-item"><button class="mv-stepper-trigger"><span class="mv-stepper-indicator"></span><span class="mv-stepper-text"><span class="mv-stepper-title">Shipping</span></span></button></li>
        <li class="mv-stepper-item"><button class="mv-stepper-trigger"><span class="mv-stepper-indicator"></span><span class="mv-stepper-text"><span class="mv-stepper-title">Payment</span></span></button></li>
        <li class="mv-stepper-item"><button class="mv-stepper-trigger"><span class="mv-stepper-indicator"></span><span class="mv-stepper-text"><span class="mv-stepper-title">Confirmation</span></span></button></li>
      </ol>
    </mv-stepper>
    <div style="display:flex;justify-content:space-between;align-items:center;gap:.75rem">
      <button class="mv-button" data-variant="outline" data-size="sm" id="st-demo-prev">Back</button>
      <span id="st-demo-label" style="font-size:.8125rem;color:var(--mv-fg-muted)" aria-live="polite">Step 2 of 4</span>
      <button class="mv-button" data-size="sm" id="st-demo-next">Continue</button>
    </div>
  </div>

  <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr));gap:2.5rem;align-items:start">
    <!-- Vertical with descriptions and a busy step -->
    <ol class="mv-stepper" data-orientation="vertical" aria-label="Package tracking">
      <li class="mv-stepper-item" data-state="completed">
        <span class="mv-stepper-indicator"></span>
        <span class="mv-stepper-text"><span class="mv-stepper-title">Order confirmed</span><span class="mv-stepper-description">Mon, Sep 21, 2:32 PM</span></span>
      </li>
      <li class="mv-stepper-item" data-state="completed">
        <span class="mv-stepper-indicator"></span>
        <span class="mv-stepper-text"><span class="mv-stepper-title">Package shipped</span><span class="mv-stepper-description">Chicago warehouse · UPS</span></span>
      </li>
      <li class="mv-stepper-item" data-state="current" aria-current="step" aria-busy="true">
        <span class="mv-stepper-indicator"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/></svg></span>
        <span class="mv-stepper-text"><span class="mv-stepper-title">Out for delivery</span><span class="mv-stepper-description">Estimated arrival tomorrow by 1 PM</span></span>
      </li>
      <li class="mv-stepper-item" data-state="upcoming">
        <span class="mv-stepper-indicator"></span>
        <span class="mv-stepper-text"><span class="mv-stepper-title">Delivered</span><span class="mv-stepper-description">Signature required</span></span>
      </li>
    </ol>

    <!-- Dots, CSS only -->
    <div style="display:grid;gap:1.5rem">
      <ol class="mv-stepper" data-variant="dots" aria-label="Account setup">
        <li class="mv-stepper-item" data-state="completed"><span class="mv-stepper-indicator"></span><span class="mv-stepper-text"><span class="mv-stepper-title">Profile</span></span></li>
        <li class="mv-stepper-item" data-state="current" aria-current="step"><span class="mv-stepper-indicator"></span><span class="mv-stepper-text"><span class="mv-stepper-title">Team</span></span></li>
        <li class="mv-stepper-item" data-state="upcoming"><span class="mv-stepper-indicator"></span><span class="mv-stepper-text"><span class="mv-stepper-title">Done</span></span></li>
      </ol>
      <ol class="mv-stepper" aria-label="File import" style="--mv-stepper-size:1.75rem;--mv-stepper-accent:var(--mv-success)">
        <li class="mv-stepper-item" data-state="completed"><span class="mv-stepper-indicator"></span></li>
        <li class="mv-stepper-item" data-state="completed"><span class="mv-stepper-indicator"></span></li>
        <li class="mv-stepper-item" data-state="current" aria-current="step"><span class="mv-stepper-indicator"></span></li>
        <li class="mv-stepper-item" data-state="upcoming"><span class="mv-stepper-indicator"></span></li>
        <li class="mv-stepper-item" data-state="upcoming"><span class="mv-stepper-indicator"></span></li>
      </ol>
    </div>
  </div>
</div>
<script type="module">
  const stepper = document.getElementById("st-demo-checkout");
  const label = document.getElementById("st-demo-label");
  const prev = document.getElementById("st-demo-prev");
  const next = document.getElementById("st-demo-next");
  const sync = (v) => {
    label.textContent = v > 4 ? "Order placed" : `Step ${v} of 4`;
    prev.disabled = v <= 1;
    next.disabled = v > 4;
    next.textContent = v >= 4 ? "Place order" : "Continue";
  };
  prev?.addEventListener("click", () => stepper.prev());
  next?.addEventListener("click", () => stepper.next());
  stepper?.addEventListener("mv-change", (e) => sync(e.detail.value));
  if (stepper) sync(Number(stepper.getAttribute("value")));
</script>

API

Attributes

NameTypeDefaultDescription
valuenumber1On <mv-stepper>: current step (1-based). Previous steps become completed; step count + 1 = all completed.
linearbooleanOn <mv-stepper>: steps after the current one are not clickable.
data-statecompleted | current | upcomingOn .mv-stepper-item (set by <mv-stepper>, or by hand for pure CSS).
data-orientationhorizontal | verticalhorizontalOn .mv-stepper: layout and connector direction.
data-variantdotsOn .mv-stepper: indicators shrunk to dots.
aria-busytrueOn a step: loading ring around the indicator.

Properties

NameTypeDescription
countnumberNumber of steps (read-only).

Methods

NameDescription
goTo(step)Goes to a step and emits mv-change.
next() / prev()Next / previous step.

Events

NameDescription
mv-changeCancelable. detail: { value, previous, completed }.

CSS classes

NameDescription
mv-stepperOn <ol aria-label>.
mv-stepper-itemA step (<li>).
mv-stepper-indicatorBadge: automatic number when empty, icon otherwise, checkmark when completed.
mv-stepper-text / -title / -descriptionStep labels.
mv-stepper-trigger<button> wrapping the indicator and text to make the step clickable.

CSS variables

NameDefaultDescription
--mv-stepper-accentvar(--mv-accent)Color of completed/current steps and of the fill.
--mv-stepper-size2remIndicator diameter.

Accessibility

Labeled ordered list; the current step has aria-current="step". Numbers and the checkmark are decorative (the title carries the meaning); add hidden text (.mv-sr-only “completed”) if the state must be announced. Clickable steps are real <button>s, disabled past the current one in linear mode. Reduced motion: no loading spin, instant fills.