Skip to content
Español

Navigate

Type to search. Press Escape to close.

    Components · js

    Stepper

    A long form cut into steps: a numbered index with state, one panel per step, and controls that move between them. The server sends an ordered list of links and the panels one under the other, so the whole form is readable and fillable without a script. On init only the current panel stays visible, the index marks what is done, current and still to come, and data-iv-step buttons refuse to advance while the fields of the panel are invalid.

    CSS: css/components/stepper.css (in ivolt.css). JS: @intervolutions/ivolt/stepper or auto.js. Added in v0.6.

    Checkout in three stepsfixtures/stepper/basic.htmlOpen alone — Checkout in three steps
    <form class="iv-form" data-iv-component="form" novalidate aria-describedby="step-note">
      <p class="iv-u-text-sm iv-u-text-muted" id="step-note">Demo checkout: nothing is submitted, no order is placed and no payment is taken. The fields are validated in the browser so the steps can refuse to advance, and the last button only announces that the flow is complete.</p>
      <div class="iv-stepper" data-iv-component="stepper">
        <ol class="iv-stepper__list" aria-label="Checkout">
          <li class="iv-stepper__step" data-iv-state="current">
            <a class="iv-stepper__trigger" href="#step-account" aria-current="step">
              <span class="iv-stepper__marker" aria-hidden="true">1</span>
              <span class="iv-stepper__label">Account</span>
              <span class="iv-stepper__hint">Email and team</span>
            </a>
          </li>
          <li class="iv-stepper__step">
            <a class="iv-stepper__trigger" href="#step-payment">
              <span class="iv-stepper__marker" aria-hidden="true">2</span>
              <span class="iv-stepper__label">Payment</span>
              <span class="iv-stepper__hint">Card holder and country</span>
            </a>
          </li>
          <li class="iv-stepper__step">
            <a class="iv-stepper__trigger" href="#step-review">
              <span class="iv-stepper__marker" aria-hidden="true">3</span>
              <span class="iv-stepper__label">Review</span>
              <span class="iv-stepper__hint">Confirm the demo</span>
            </a>
          </li>
        </ol>
        <div class="iv-stepper__progress" aria-hidden="true"></div>
        <div class="iv-stepper__panels">
          <section class="iv-stepper__panel" id="step-account" aria-labelledby="step-account-title" tabindex="-1">
            <h3 class="iv-u-mt-0" id="step-account-title">Account</h3>
            <div class="iv-field">
              <label class="iv-label" for="step-email">Work email <span class="iv-label__required" aria-hidden="true">*</span></label>
              <input class="iv-input" id="step-email" name="email" type="email" required autocomplete="email">
            </div>
            <div class="iv-field">
              <label class="iv-label" for="step-team">Team name <span class="iv-label__required" aria-hidden="true">*</span></label>
              <input class="iv-input" id="step-team" name="team" type="text" required minlength="2" autocomplete="organization">
            </div>
            <div class="iv-stepper__controls" hidden>
              <button class="iv-button iv-button--secondary" type="button" data-iv-step="prev" disabled>Back</button>
              <button class="iv-button iv-button--primary" type="button" data-iv-step="next">Continue</button>
            </div>
          </section>
          <section class="iv-stepper__panel" id="step-payment" aria-labelledby="step-payment-title" tabindex="-1">
            <h3 class="iv-u-mt-0" id="step-payment-title">Payment</h3>
            <div class="iv-field">
              <label class="iv-label" for="step-holder">Card holder <span class="iv-label__required" aria-hidden="true">*</span></label>
              <input class="iv-input" id="step-holder" name="holder" type="text" required autocomplete="cc-name">
            </div>
            <div class="iv-field">
              <label class="iv-label" for="step-country">Billing country <span class="iv-label__required" aria-hidden="true">*</span></label>
              <select class="iv-select" id="step-country" name="country" required>
                <option value="">Select a country</option>
                <option value="es">Spain</option>
                <option value="pt">Portugal</option>
                <option value="fr">France</option>
              </select>
            </div>
            <p class="iv-u-text-sm iv-u-text-muted iv-u-m-0">No card number is asked for: this demo has nothing to charge.</p>
            <div class="iv-stepper__controls" hidden>
              <button class="iv-button iv-button--secondary" type="button" data-iv-step="prev">Back</button>
              <button class="iv-button iv-button--primary" type="button" data-iv-step="next">Continue</button>
            </div>
          </section>
          <section class="iv-stepper__panel" id="step-review" aria-labelledby="step-review-title" tabindex="-1">
            <h3 class="iv-u-mt-0" id="step-review-title">Review</h3>
            <p>Nothing on this page leaves the browser. Confirming only emits the completion event of the component, which a real site would use to submit its own form.</p>
            <div class="iv-field">
              <label class="iv-check"><input id="step-confirm" name="confirm" type="checkbox" required> I understand this demo sends nothing <span class="iv-label__required" aria-hidden="true">*</span></label>
            </div>
            <div class="iv-stepper__controls" hidden>
              <button class="iv-button iv-button--secondary" type="button" data-iv-step="prev">Back</button>
              <button class="iv-button iv-button--primary" type="button" data-iv-step="next">Finish the demo</button>
            </div>
          </section>
        </div>
      </div>
    </form>
    // data-iv-component="form"
    // data-iv-component="stepper"
    {
      state: "current",
      step: "prev",
    }

    Vertical index

    iv-stepper--vertical moves the index into a column beside the panels from the md breakpoint up, with the connector running down instead of across. Below that breakpoint it is the same stacked layout as the default.

    Vertical indexfixtures/stepper/vertical.htmlOpen alone — Vertical index
    <div class="iv-stepper iv-stepper--vertical" data-iv-component="stepper" data-iv-validate="false">
      <ol class="iv-stepper__list" aria-label="Release">
        <li class="iv-stepper__step">
          <a class="iv-stepper__trigger" href="#rel-draft">
            <span class="iv-stepper__marker" aria-hidden="true">1</span>
            <span class="iv-stepper__label">Draft</span>
            <span class="iv-stepper__hint">Write the notes</span>
          </a>
        </li>
        <li class="iv-stepper__step">
          <a class="iv-stepper__trigger" href="#rel-review">
            <span class="iv-stepper__marker" aria-hidden="true">2</span>
            <span class="iv-stepper__label">Review</span>
            <span class="iv-stepper__hint">Two pairs of eyes</span>
          </a>
        </li>
        <li class="iv-stepper__step">
          <a class="iv-stepper__trigger" href="#rel-build">
            <span class="iv-stepper__marker" aria-hidden="true">3</span>
            <span class="iv-stepper__label">Build</span>
            <span class="iv-stepper__hint">Sizes and tests</span>
          </a>
        </li>
        <li class="iv-stepper__step">
          <a class="iv-stepper__trigger" href="#rel-ship">
            <span class="iv-stepper__marker" aria-hidden="true">4</span>
            <span class="iv-stepper__label">Ship</span>
            <span class="iv-stepper__hint">Only with a signature</span>
          </a>
        </li>
      </ol>
      <div class="iv-stepper__panels">
        <section class="iv-stepper__panel" id="rel-draft" aria-labelledby="rel-draft-title" tabindex="-1">
          <h3 class="iv-u-mt-0" id="rel-draft-title">Draft</h3>
          <p>Sample flow, invented for this fixture: no release is prepared or published from this page. The vertical variant keeps the index in a column beside the panels from the <code>md</code> breakpoint upwards.</p>
          <div class="iv-stepper__controls" hidden>
            <button class="iv-button iv-button--secondary" type="button" data-iv-step="prev" disabled>Back</button>
            <button class="iv-button iv-button--primary" type="button" data-iv-step="next">Continue</button>
          </div>
        </section>
        <section class="iv-stepper__panel" id="rel-review" aria-labelledby="rel-review-title" tabindex="-1">
          <h3 class="iv-u-mt-0" id="rel-review-title">Review</h3>
          <p>The connector between markers fills up to the step you are on, and every step already done shows a check instead of its number.</p>
          <div class="iv-stepper__controls" hidden>
            <button class="iv-button iv-button--secondary" type="button" data-iv-step="prev">Back</button>
            <button class="iv-button iv-button--primary" type="button" data-iv-step="next">Continue</button>
          </div>
        </section>
        <section class="iv-stepper__panel" id="rel-build" aria-labelledby="rel-build-title" tabindex="-1">
          <h3 class="iv-u-mt-0" id="rel-build-title">Build</h3>
          <p>There is nothing to validate in this fixture, so the panels advance freely: the steps carry text, not fields.</p>
          <div class="iv-stepper__controls" hidden>
            <button class="iv-button iv-button--secondary" type="button" data-iv-step="prev">Back</button>
            <button class="iv-button iv-button--primary" type="button" data-iv-step="next">Continue</button>
          </div>
        </section>
        <section class="iv-stepper__panel" id="rel-ship" aria-labelledby="rel-ship-title" tabindex="-1">
          <h3 class="iv-u-mt-0" id="rel-ship-title">Ship</h3>
          <p>The last step does not publish anything: it announces that the flow is complete and leaves the decision to whoever is reading.</p>
          <div class="iv-stepper__controls" hidden>
            <button class="iv-button iv-button--secondary" type="button" data-iv-step="prev">Back</button>
            <button class="iv-button iv-button--primary" type="button" data-iv-step="next">Finish the demo</button>
          </div>
        </section>
      </div>
    </div>
    // data-iv-component="stepper"
    {
      validate: false,
      step: "prev",
    }

    Compact

    iv-stepper--compact draws the markers and the label of the current step only; the other labels stay in the page for screen readers, so no step loses its name. This one also runs with data-iv-linear="false", so every step is reachable from the index at any time.

    Compact, non-linearfixtures/stepper/compact.htmlOpen alone — Compact, non-linear
    <div class="iv-stepper iv-stepper--compact" data-iv-component="stepper" data-iv-linear="false" data-iv-validate="false">
      <ol class="iv-stepper__list" aria-label="Onboarding">
        <li class="iv-stepper__step">
          <a class="iv-stepper__trigger" href="#onb-profile">
            <span class="iv-stepper__marker" aria-hidden="true">1</span>
            <span class="iv-stepper__label">Profile</span>
          </a>
        </li>
        <li class="iv-stepper__step">
          <a class="iv-stepper__trigger" href="#onb-workspace">
            <span class="iv-stepper__marker" aria-hidden="true">2</span>
            <span class="iv-stepper__label">Workspace</span>
          </a>
        </li>
        <li class="iv-stepper__step">
          <a class="iv-stepper__trigger" href="#onb-invite">
            <span class="iv-stepper__marker" aria-hidden="true">3</span>
            <span class="iv-stepper__label">Invites</span>
          </a>
        </li>
        <li class="iv-stepper__step">
          <a class="iv-stepper__trigger" href="#onb-done">
            <span class="iv-stepper__marker" aria-hidden="true">4</span>
            <span class="iv-stepper__label">Done</span>
          </a>
        </li>
      </ol>
      <div class="iv-stepper__progress" aria-hidden="true"></div>
      <div class="iv-stepper__panels">
        <section class="iv-stepper__panel" id="onb-profile" aria-labelledby="onb-profile-title" tabindex="-1">
          <h3 class="iv-u-mt-0" id="onb-profile-title">Profile</h3>
          <p>Sample onboarding, invented for this fixture: no account exists and nothing is saved. In the compact variant only the markers and the label of the current step are drawn; the other labels stay in the page for screen readers, so every step keeps its name.</p>
          <div class="iv-stepper__controls" hidden>
            <button class="iv-button iv-button--secondary" type="button" data-iv-step="prev" disabled>Back</button>
            <button class="iv-button iv-button--primary" type="button" data-iv-step="next">Continue</button>
          </div>
        </section>
        <section class="iv-stepper__panel" id="onb-workspace" aria-labelledby="onb-workspace-title" tabindex="-1">
          <h3 class="iv-u-mt-0" id="onb-workspace-title">Workspace</h3>
          <p>This stepper is not linear, so every step can be reached from the index at any time and the arrow keys walk the whole row.</p>
          <div class="iv-stepper__controls" hidden>
            <button class="iv-button iv-button--secondary" type="button" data-iv-step="prev">Back</button>
            <button class="iv-button iv-button--primary" type="button" data-iv-step="next">Continue</button>
          </div>
        </section>
        <section class="iv-stepper__panel" id="onb-invite" aria-labelledby="onb-invite-title" tabindex="-1">
          <h3 class="iv-u-mt-0" id="onb-invite-title">Invites</h3>
          <p>No invitation is sent from here: there is no address book and no mail behind this page.</p>
          <div class="iv-stepper__controls" hidden>
            <button class="iv-button iv-button--secondary" type="button" data-iv-step="prev">Back</button>
            <button class="iv-button iv-button--primary" type="button" data-iv-step="next">Continue</button>
          </div>
        </section>
        <section class="iv-stepper__panel" id="onb-done" aria-labelledby="onb-done-title" tabindex="-1">
          <h3 class="iv-u-mt-0" id="onb-done-title">Done</h3>
          <p>The progress bar above reads the same custom property the connector uses, so both tell the same story.</p>
          <div class="iv-stepper__controls" hidden>
            <button class="iv-button iv-button--secondary" type="button" data-iv-step="prev">Back</button>
            <button class="iv-button iv-button--primary" type="button" data-iv-step="next">Finish the demo</button>
          </div>
        </section>
      </div>
    </div>
    // data-iv-component="stepper"
    {
      linear: false,
      validate: false,
      step: "prev",
    }

    Options

    OptionAttributeDefaultEffect
    lineardata-iv-lineartrueOnly steps already visited and the current one are reachable from the index; the rest carry aria-disabled="true". Going back and jumping forward again does not revalidate.
    validatedata-iv-validatetrueCheck the controls of the current panel before advancing.
    hashdata-iv-hashfalseKeep location.hash in sync with the current panel and answer hashchange.
    focusdata-iv-focuspanelWhere focus lands after a change: the new panel, or none.
    statusTextdata-iv-status-textStep {index} of {total}: {label}Sentence announced in the polite live region after every change.

    Methods and events

    go(index, reason), next(), prev(), reset(), setState(index, state), destroy(); read index (0-based), steps, isFirst and isLast. setState is how a server answer marks a step error or done from your own code.

    EventWhendetail
    iv:changeiv:changedbefore and after moving; the first is cancelableindex, previousIndex, reason: trigger, next, prev, api or hash
    iv:completenext on the last step, once its validation has passedindex

    The component never submits anything. iv:complete is where your code sends the form, and cancelling iv:change is where it refuses a move for a reason of its own.

    Validation

    With validate on, next checks the current panel first. If the panel sits inside a <form> that has a Form instance, every control in the panel goes through validateField and the messages appear where that component puts them; otherwise the browser's own reportValidity() stops on the first invalid control. Going back never validates. Asynchronous checks, conditional steps and draft saving are out of scope: do them in your own handler and call go() or setState() with the answer.

    Keyboard and assistive technology

    The index is an <ol> of links with a label, and the current step carries aria-current="step". Left and Right, Home and End move focus between reachable triggers; Enter or Space activates one. The markers are aria-hidden decoration — the number they draw is already the list order — and each panel is a section with a heading and tabindex="-1" so focus can land on it. State is announced in a visually hidden polite region, and a step in error adds a hidden "has errors" to its label, so colour is never the only carrier.

    Without JavaScript

    Every panel is served visible, one under the other, and the index is a list of anchors that jump to each section. The next and back buttons are served inside a hidden container, because a button that cannot advance is worse than no button; init reveals them. Nothing is hidden and no field is unreachable: the form is one long form, which is exactly what it was before it was cut into steps.

    Tokens and locals

    LocalDefaultWhat it moves
    --iv-stepper-marker2remDiameter of the numbered circle.
    --iv-stepper-linevar(--iv-color-border)Colour of the connector between markers.
    --iv-stepper-fillvar(--iv-color-primary)Colour of the part of the connector already walked.
    --iv-stepper-track2pxThickness of the connector and of the progress bar.
    --iv-stepper-gapvar(--iv-space-6)Space between the index and the panels.
    --iv-stepper-step-gapvar(--iv-space-5)Space between steps in the index.

    The component writes --iv-stepper-progress (0 to 1) on the root, which is what the optional iv-stepper__progress bar scales. Each step carries data-iv-state with done, current, upcoming or error: a done marker shows a check instead of its number, the current one takes the shared focus halo, and an error marker turns to the danger colour. Under reduced motion the panels appear without travelling.

    Common mistakes

    • Steps with no heading. Each panel needs its own heading: it is the name of the step for anyone who cannot see the index.
    • Serving the panels hidden, or the controls visible. The first loses the form without a script; the second offers buttons that do nothing.
    • More than five or six steps. Past that, an index is a table of contents and the form wants splitting into pages.
    • Expecting the component to submit. It announces completion; sending the data stays yours.