Stepper

Free

7 variants — six wizard styles (default, pill, minimal, icon, dot, line) plus a non-interactive tracker — all free, plus horizontal/vertical orientation. Linear wizard mode, guard rules, and telemetry are available in the Pro package.

</>sv-stepper stepperwizardnavigation
import { SvStepperComponent } from '@styloviz/stepper';

Demo & usage

Basic usage

Usage example — open the gallery for the full interactive demo.

An interactive demo for this component lives in the gallery.

Open live demo

More examples

All 7 variants (free)

example.htmlAngular
<sv-stepper [items]="steps" variant="line" orientation="vertical" [activeIndex]="step" />

Vertical linear wizard (Pro)

example.htmlAngular
<sv-stepper-pro
  [items]="steps"
  orientation="vertical"
  [linear]="true"
  [interactive]="true"
  [showDescriptions]="true"
  [activeIndex]="step"
  (stepChange)="step = $event"
  (stepBlocked)="onBlocked($event)" />

Order status tracker (placed → delivered)

variant="tracker" renders a horizontal fulfilment tracker: completed stages fill the rail, the current stage is ringed, each with an icon and timestamp.

example.htmlAngular
<sv-stepper variant="tracker" [items]="orderSteps" [activeIndex]="3" [interactive]="false" />

When to use

  • Job: workflow state — renders step buttons and emits step events; it never navigates the page (compare all seven in the “Choosing a navigation pattern” guide under Guides).
  • Guiding users through a sequential, multi-step process (checkout, onboarding, wizards).
  • You want progress indication, and — on the Pro package — per-step validation gating.
  • For non-sequential views, use Tabs; for an overlay walkthrough, a Product Tour.

Async states

  • loading
  • error
  • retry

Saved state

Stored kinds
  • sv-stepper/resume

What you will find under your own storage key. You choose the key; the blob names its own kind and schema version, so a component never reads state another one wrote.

Reading it back
Restore through the component's own restore… helper rather than casting the parsed JSON. A blob from an older release is migrated where it can be and refused with a reason where it cannot — so an upgrade never silently applies last month's shape to this month's fields.

Inputs

NameTypeDefaultDescription
itemsStepperItem[][]The steps to render (label, optional description, state).
activeIndexnumber (two-way)00-based active step index. Supports two-way binding via `[(activeIndex)]`.
orientationStepperOrientation'horizontal'Layout: horizontal or vertical.
variantStepperVariant'default'Visual variant — and with it, whether this is a wizard or a tracker. `default`, `pill`, `minimal`, `icon`, `dot` and `line` are WIZARD styles: every step is a `<button>`, and the linear gate, keyboard, `stepClick` and the unsaved prompt are all live. `'tracker'` is a PROGRESS DISPLAY (an order-status strip): it renders a non-interactive `<ol>` with no buttons, so nothing in it can be clicked, focused or arrowed through and `stepClick` / `stepChangeRequest` never fire from it. Two axes share this one input — look and interactivity. Choose `tracker` to show where something IS, not to let the user move it; for a wizard's look without its navigation, keep a wizard style and set `interactive=false` instead.
sizeStepperSize'md'Step size: sm, md or lg.
modeStepperMode'standard'Navigation mode: standard sequence, or branch (conditional paths via `activeBranch`).
interactivebooleantrueWhether users can click steps to navigate.
showDescriptionsbooleantrueShow step descriptions under labels.
showConnectorsbooleantrueRender connectors between steps.
enableHotkeysbooleanfalseEnables advanced keyboard shortcuts for rapid step navigation.
activeBranchstring | null (two-way)nullActive branch key used in `mode='branch'`. Supports two-way binding.
customClassstring''Extra CSS classes for the root element.
urlKeystring | nullnullPut the current step's **id** in the URL under this key, so a step can be shared and the back button moves one step back. Needs `provideSvNavLocation()` at the app root.
surfaceIdstring | nullnullNames this stepper in the shared navigation-analytics stream, when a page has more than one. Ignored unless `provideSvNavTelemetry()` is wired up.
persistKeystring''Where `localStorage` keeps this workflow's position, so a reload resumes rather than restarting. Stores the step id and branch only — never the host's form data. Empty = no persistence.
asyncValidationbooleanfalseAsk before moving. With this on, activating a step emits `stepChangeRequest` and nothing happens until the host calls `allow()` or `deny()` — which is what lets a wizard validate the current step against the server before letting the user leave it.
validatingIndexnumber-1Index currently being validated, or `-1`. That step shows a spinner and the stepper ignores further activations, so a double-click cannot start two validations.
revisitPolicyStepperRevisitPolicy'preserve'What returning to an earlier step does to the steps after it. See StepperRevisitPolicy. `'preserve'` IS TODAY'S BEHAVIOUR, and that is why it is the default: before this input there was no defined effect at all, so anything else would start invalidating work in wizards that never asked for it.
invalidatesOnRevisit((context: StepperRevisitContext) => readonly string[]) | nullnullWith `revisitPolicy="custom"`, the step ids a revisit invalidates. Ids that are not visible steps are ignored. ```ts protected readonly recomputeTotals = (c: StepperRevisitContext) => c.step.id === 'billing' ? ['shipping', 'confirm']: []; ```
dirtybooleanfalseIs there unsaved work in the step the user is on? A WIZARD STEP'S FORM COULD BE ABANDONED BY ONE CLICK ON ANOTHER STEP, with no warning and no undo. Set this and the stepper ASKS instead of moving — from `createCloseGuard`, the same primitive Modal, Drawer and Confirm Dialog use, so the prompt behaves identically in all four and the order of the questions is decided once (`docs/close-policy-adr.md`).
canLeaveStep((reason: StepperLeaveReason) => boolean | void) | nullnullThe host's own veto on leaving a step, consulted **before** the unsaved check. Return `false` to refuse; return nothing to have no opinion. A veto is silent — it does NOT raise the unsaved-changes prompt, because the host refused for its own reasons and owns its own messaging. That ordering is the primitive's, not this component's.
showUnsavedPromptbooleantrueRender the built-in unsaved-changes prompt. Turn it off to drive your own dialog from `promptingDiscard()` / `discardChanges()` / `keepEditing()`.
unsavedTitlestring'Leave this step?'Heading of the unsaved-changes prompt.
unsavedMessagestring'This step has changes that have not be…Body of the unsaved-changes prompt.
discardLabelstring'Discard'Label of the button that leaves and loses the changes.
keepEditingLabelstring'Keep editing'Label of the button that stays on the step.
loadingbooleanfalseTrue while the step definition is being fetched. A wizard defined server-side — onboarding flows, dynamic forms — arrives asynchronously.
errorSvDataError | string | nullnullFailure loading the step definition. Rendered in place of the content, with a retry.

Outputs

EventPayload typeDescription
retryvoidThe host should re-run the failed load.
stepChangeStepperChangeEventEmitted when the active step changes.
unresolvedSvUnresolvedValue<number>Emitted when `activeIndex` names a step that does not exist. Carries what was asked for, what is shown, and whether `activeIndex` was corrected.
stepClick{ index: number; step: StepperItem }Emitted when a step is clicked.
stepBlockedStepperBlockedEventEmitted when navigation to a step is blocked (e.g. linear validation).
stepChangeRequestStepperChangeRequestA move the host must approve. Only emitted when `asyncValidation` is on. See StepperChangeRequest.
branchChangeStepperBranchChangeEventEmitted when the active branch changes (branch mode).
stepTransition{ readonly step: StepperItem; readonly index: number; readonly transition: StepperTransition; readonly from: StepperStepLifecycle; readonly to: StepperStepLifecycle; }A named lifecycle transition just happened. The transition, not just the destination. `enter` and `reopen` both land in `active`, and a host that invalidates later work needs to know which of the two it was; `stepChange` cannot tell it.
stepsInvalidatedStepperInvalidationEventThe revisit policy says these steps are no longer done. See StepperInvalidationEvent.
flowStatusChangeStepperFlowStatus`complete` and `valid`, reported separately — see StepperFlowStatus.
discardedStepperLeaveReasonThe user chose to abandon unsaved work; the move follows.
leaveCancelledStepperLeaveReasonThe user chose to stay on the step. Nothing moved.
activeIndexChangenumberEmits the new `activeIndex` whenever it changes — the output half of `[(activeIndex)]`.
activeBranchChangestring | nullEmits the new `activeBranch` whenever it changes — the output half of `[(activeBranch)]`.

<sv-stepper-pro>

Also in this family — SvStepperProComponent from @styloviz-pro/stepper.

Async states

  • loading
  • error
  • retry

Saved state

Stored kinds
  • sv-stepper-pro/resume

What you will find under your own storage key. You choose the key; the blob names its own kind and schema version, so a component never reads state another one wrote.

Reading it back
Restore through the component's own restore… helper rather than casting the parsed JSON. A blob from an older release is migrated where it can be and refused with a reason where it cannot — so an upgrade never silently applies last month's shape to this month's fields.

Inputs

NameTypeDefaultDescription
itemsStepperItem[][]The steps, in order. In `mode="branch"` a step with a `branch` key is shown only while that branch is active.
activeIndexnumber (two-way)00-based active step index. Supports two-way binding via `[(activeIndex)]`.
orientationStepperOrientation'horizontal'Lay the steps out in a row (`horizontal`) or a column (`vertical`).
variantStepperVariant'default'Visual variant — and with it, whether this is a wizard or a tracker. `default`, `pill`, `minimal`, `icon`, `dot` and `line` are WIZARD styles: every step is a `<button>`, and the linear gate, keyboard, `stepClick` and the unsaved prompt are all live. `'tracker'` is a PROGRESS DISPLAY (an order-status strip): it renders a non-interactive `<ol>` with no buttons, so nothing in it can be clicked, focused or arrowed through and `stepClick` / `stepChangeRequest` never fire from it. Two axes share this one input — look and interactivity. Choose `tracker` to show where something IS, not to let the user move it; for a wizard's look without its navigation, keep a wizard style and set `interactive=false` instead.
sizeStepperSize'md'Size of the step indicators, labels and descriptions.
modeStepperMode'standard'`branch` enables conditional workflows: steps are filtered by `activeBranch`, and a step's `selectsBranch` switches it.
linearbooleanfalseIf true, blocked future steps cannot be selected unless prior steps are complete.
interactivebooleantrueWhether users can click steps to navigate.
showDescriptionsbooleantrueShow step descriptions under labels.
showConnectorsbooleantrueRender connectors between steps.
enableHotkeysbooleanfalseEnables advanced keyboard shortcuts for rapid step navigation.
guardRulesRecord<string, string[]>{}Optional guard map where each key is a target step id and the value lists step ids that must be complete before selecting that target.
activeBranchstring | null (two-way)nullActive branch key used in `mode='branch'`. Supports two-way binding.
customClassstring''Extra CSS classes added to the root element.
urlKeystring | nullnullPut the current step's **id** in the URL under this key, so a step can be shared and the back button moves one step back. Needs `provideSvNavLocation()` at the app root.
surfaceIdstring | nullnullNames this stepper in the shared navigation-analytics stream, when a page has more than one. Ignored unless `provideSvNavTelemetry()` is wired up.
persistKeystring''Where `localStorage` keeps this workflow's position, so a reload resumes rather than restarting. Stores the step id and branch only — never the host's form data. Empty = no persistence.
asyncValidationbooleanfalseAsk before moving. With this on, activating a step emits `stepChangeRequest` and nothing happens until the host calls `allow()` or `deny()` — which is what lets a wizard validate the current step against the server before letting the user leave it.
validatingIndexnumber-1Index currently being validated, or `-1`. That step shows a spinner and the stepper ignores further activations, so a double-click cannot start two validations.
revisitPolicyStepperRevisitPolicy'preserve'What returning to an earlier step does to the steps after it. See StepperRevisitPolicy. `'preserve'` IS TODAY'S BEHAVIOUR, and that is why it is the default: before this input there was no defined effect at all, so anything else would start invalidating work in wizards that never asked for it.
invalidatesOnRevisit((context: StepperRevisitContext) => readonly string[]) | nullnullWith `revisitPolicy="custom"`, the step ids a revisit invalidates. Ids that are not visible steps are ignored. ```ts protected readonly recomputeTotals = (c: StepperRevisitContext) => c.step.id === 'billing' ? ['shipping', 'confirm']: []; ```
dirtybooleanfalseIs there unsaved work in the step the user is on? A WIZARD STEP'S FORM COULD BE ABANDONED BY ONE CLICK ON ANOTHER STEP, with no warning and no undo. Set this and the stepper ASKS instead of moving — from `createCloseGuard`, the same primitive Modal, Drawer and Confirm Dialog use, so the prompt behaves identically in all four and the order of the questions is decided once (`docs/close-policy-adr.md`).
canLeaveStep((reason: StepperLeaveReason) => boolean | void) | nullnullThe host's own veto on leaving a step, consulted **before** the unsaved check. Return `false` to refuse; return nothing to have no opinion. A veto is silent — it does NOT raise the unsaved-changes prompt, because the host refused for its own reasons and owns its own messaging. That ordering is the primitive's, not this component's.
showUnsavedPromptbooleantrueRender the built-in unsaved-changes prompt. Turn it off to drive your own dialog from `promptingDiscard()` / `discardChanges()` / `keepEditing()`.
unsavedTitlestring'Leave this step?'Heading of the unsaved-changes prompt.
unsavedMessagestring'This step has changes that have not be…Body of the unsaved-changes prompt.
discardLabelstring'Discard'Label of the button that leaves and loses the changes.
keepEditingLabelstring'Keep editing'Label of the button that stays on the step.
loadingbooleanfalseTrue while the step definition is being fetched. A wizard defined server-side — onboarding flows, dynamic forms — arrives asynchronously.
errorSvDataError | string | nullnullFailure loading the step definition. Rendered in place of the content, with a retry.

Outputs

EventPayload typeDescription
retryvoidThe host should re-run the failed load.
stepChangeStepperChangeEventThe active step changed (click, keyboard, URL or an approved `stepChangeRequest`). Carries the previous and new index and the new step.
unresolvedSvUnresolvedValue<number>Emitted when `activeIndex` names a step that does not exist. Carries what was asked for, what is shown, and whether `activeIndex` was corrected.
stepClick{ index: number; step: StepperItem }A step was pressed, before any gate or validation runs; fires even if the move is then blocked. Carries its index and item.
stepBlockedStepperBlockedEventA move was refused by a disabled step, the `linear` gate, `guardRules`, or an async `deny()`. Carries the target step and the reason.
stepChangeRequestStepperChangeRequestA move the host must approve. Only emitted when `asyncValidation` is on. See StepperChangeRequest.
branchChangeStepperBranchChangeEventActivating a step with `selectsBranch` switched the active branch. Carries the new branch and the step that selected it.
telemetryStepperTelemetryEventAnalytics stream for the whole flow — emits `guard_hit`, `blocked` and `branch_switch` events with the step id, index, active branch and block reason.
stepTransition{ readonly step: StepperItem; readonly index: number; readonly transition: StepperTransition; readonly from: StepperStepLifecycle; readonly to: StepperStepLifecycle; }A named lifecycle transition just happened. The transition, not just the destination. `enter` and `reopen` both land in `active`, and a host that invalidates later work needs to know which of the two it was; `stepChange` cannot tell it.
stepsInvalidatedStepperInvalidationEventThe revisit policy says these steps are no longer done. See StepperInvalidationEvent.
flowStatusChangeStepperFlowStatus`complete` and `valid`, reported separately — see StepperFlowStatus.
discardedStepperLeaveReasonThe user chose to abandon unsaved work; the move follows.
leaveCancelledStepperLeaveReasonThe user chose to stay on the step. Nothing moved.
activeIndexChangenumberEmits the new `activeIndex` whenever it changes — the output half of `[(activeIndex)]`.
activeBranchChangestring | nullEmits the new `activeBranch` whenever it changes — the output half of `[(activeBranch)]`.

Wire it to your API

This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.

Template

Bound to the names this component actually exposes.

usage.htmlAngular
<sv-stepper
  [items]="data.items()"
  [loading]="data.loading()"
  [error]="data.error()"
  (retry)="data.reload()"
/>

Host class

Drive the three async inputs from your own fetch.

host.component.tsTypeScript
// This component reports its state but does not emit a query, so drive
// it from your own fetch and bind the three async inputs.
readonly items = signal<Row[]>([]);
readonly loading = signal(false);
readonly error = signal<SvDataError | null>(null);

async load(): Promise<void> {
  this.loading.set(true);
  this.error.set(null);
  try {
    this.items.set(await this.api.fetchRows());
  } catch (e) {
    this.error.set(toSvDataError(e));
  } finally {
    this.loading.set(false);
  }
}
  • This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `items`, `loading` and `error`.
  • Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.

Full walkthrough: Connecting to a backend.

Customization & theming

  • Six visual variants — default, pill, minimal, icon, dot, line — all included in the free package.
  • Horizontal or vertical orientation.
  • Pro adds: linear wizard mode with per-step validation, guard rules, branch telemetry, and roving-tabindex hotkeys.

Best practices

  • Do: Use `[linear]="true"` (Pro) when later steps depend on earlier ones being valid.
  • Do: Clearly mark completed, current and blocked steps.
  • Tip: Switch to vertical orientation when step descriptions are long.

Accessibility

  • Rendered as a labelled `list` of `listitem`s; each step is a real `<button>` (announced as a button) that exposes completed/current/disabled state via `aria-current="step"` and `aria-disabled`.
  • Keyboard reachable via a roving tabindex: the current step is tabbable and Arrow/Home/End move between steps, with optional number hotkeys (`enableHotkeys`).

Feedback on Stepper

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.