Angular Stepper Component | ng-hub-ui-stepper

Angular stepper component with validation, orientation modes, custom controls, i18n and CSS-variable theming.

Last updated Sep 28, 2026

Overview

Why teams search for this library

This Angular stepper component is useful for guided multi-step flows where validation, progression rules and branding need to stay under your control.

Install

npm install ng-hub-ui-stepper

Jump to

Ideal for

  • checkout flows
  • wizard forms
  • onboarding
  • configuration steps

About stepper

ng-hub-ui-stepper helps Angular applications structure onboarding, checkout and wizard-like forms. It gives teams a reusable way to control step progression, validation and navigation logic without baking those rules into each flow by hand.

Feature guides

Core Usage

Basic setup and orientation behavior.

Examples:
Basic Usage

Basic Usage

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus.

Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec ullamcorper nulla non metus.

Orientation

Orientation

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus.

Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec ullamcorper nulla non metus.

Truncated Titles & Tooltip

Truncated Titles & Tooltip

Long step titles are clipped to an ellipsis. Hover the nav title above to read the full text.

Inline track rail

Inline track rail

nav="track"
nav="pills" — the default

The Review step binds no [valid] at all, so its verdict is null: nobody has said anything, and the step counts as in order once it has been visited. The number under the tick never goes away — the marker gains a mark, it does not swap identity.

Step order vocabulary

Step order vocabulary

TermWhat it meansAccountAddressReview
visited The user has stood on this step. A jump over it does not set it, and walking back does not unset it. yesnono
valid Your verdict on this step's own data. Tri-state: true, false, or null when nobody has said. nullnullnull
inOrder The stated validity when there is one, otherwise visited. This is what the tick paints and what the forward gate reads. truefalsefalse
reachable What the track lets you activate from here: backwards always, forwards only once every step in between is in order. A disabled step is closed off both ways. truetruefalse

isCompleted, in the hubStepTrigger context, is none of these: it still answers index < currentIndex, so every template written before 22.12.0 reads the same.

Advanced Flows

Validation, i18n, RTL, API-driven navigation, custom controls and custom step headers.

Examples:
Inside a Modal

Inside a Modal

Validation

Validation

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus.

Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec ullamcorper nulla non metus.

Programmatic Control

Programmatic Control

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus.

Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec ullamcorper nulla non metus.

Event: -

Custom Navigation

Custom Navigation

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus.

Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec ullamcorper nulla non metus.

Custom Controls

Custom Controls

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus.

Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec ullamcorper nulla non metus.

Animations

Animations

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus.

Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec ullamcorper nulla non metus.

Internationalization (i18n)

Internationalization (i18n)

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus.

Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec ullamcorper nulla non metus.

Right-to-Left (RTL)

Right-to-Left (RTL)

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus.

Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec ullamcorper nulla non metus.

Reachability

Reachability

Backwards is always allowed. Forwards is allowed only once every step from the current one up to the one before the target is in order — stated valid, or, with nothing stated, visited. A disabled step is closed off either way.

  • AccountisInOrder(0) = falseisReachable(0) = true
  • AddressisInOrder(1) = falseisReachable(1) = false
  • PaymentisInOrder(2) = falseisReachable(2) = false
  • ReviewisInOrder(3) = falseisReachable(3) = false

Standing on Account with nothing ticked, goTo(2) is refused: Address is out of order. Tick Account and Address and the same click lands.

Saving before a jump

Saving before a jump

Press Continue and watch the rail: the step does not move until the save answers.

Nothing saved yet.

Styling

Runtime theme customization through the theme service.

Examples:
Theming

Theming

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus.

Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec ullamcorper nulla non metus.

Key features

Recent changes

Version 22.12.1 - 9/28/26, 12:00 AM

changed: The family list in both READMEs names `ng-hub-ui-spreadsheet`. The sheet joined the family and the other lists had not caught up. Documentation only: no code, types or styles change.

Version 22.12.0 - 9/23/26, 12:00 AM

added: hub-step takes a validity state: [valid]. Tri-state — true, false, or null when nothing has been said — because the library cannot tell "this step is fine" from "nobody has looked yet", and collapsing the two would make every existing stepper behave as if all its steps failed. From it the step derives inOrder: the stated validity when there is one, otherwise whether the user has been through the step. Position no longer stands in for either.

added: hub-step also exposes visited, set by the stepper when the step is actually reached. A jump over a step does not mark it, and walking back does not unmark the ones behind.

added: An inline track rail: nav="track". A numbered marker per step, joined by a connector, with a tick in the corner of every step that is in order — the number stays visible under it. It is a variant, not a replacement: nav defaults to pills and the rail the component has always drawn is untouched.

added: The track allows a forward jump only when the steps in between are in order, and a jump back always. isReachable(index) answers that question, isInOrder(index) the one underneath it, and both are public so a custom rail can ask them too.

added: beforeStepChange intercepts a move before it happens, so a wizard can save the step it is leaving and refuse the jump if the save fails. It runs while the stepper is still on the step being left; returning false, a promise resolving to false, or a promise that rejects cancels the move. Same contract ng-hub-ui-portal gives beforeDismiss — an output could not do this job, since by the time one fires the step has already changed.

added: The hubStepTrigger context gains visited, valid, inOrder and reachable. isCompleted is left exactly as it was — index < currentIndex — so a template written against the old context reads the same.

added: IN_ORDER joins the ten bundled dictionaries, and inOrderLabel overrides it per instance. It is the text a screen reader hears on a ticked marker.

added: Track tokens: --hub-stepper-track-*. The marker reuses the published --hub-stepper-indicator-size rather than inventing a second diameter.

fixed: goTo() walked onto disabled steps. It checked the bounds and nothing else, so a programmatic jump — or goToNext() into a disabled step — landed where the rail refused to go, and the two disagreed about the same wizard. It now asks canNavigateTo(), which is the permission the rail has always used. See BREAKING_CHANGES.md.

changed: The Continue and Submit controls now consult the step validity — the built-in buttons and the projected button[nextButton] / button[submitButton] alike, so the two never disagree about the same wizard. For a stepper that never mentions valid this is the same answer as before, to the letter: an unstated step the user is standing on is visited, so it is in order. See BREAKING_CHANGES.md.

Version 22.11.0 - 9/23/26, 12:00 AM

changed: BREAKING: the Angular floor rises from 17.2.0 to 17.3.0. The old range was measured from the source alone, and its published `.d.ts` names `InputSignalWithTransform` or `OutputEmitterRef`, which Angular did not ship until 17.3. An application below the new floor could install this package and then fail to build; it now gets the peer warning it should always have had. Nothing that worked stops working. See BREAKING_CHANGES.md.

Version 22.10.4 - 9/23/26, 12:00 AM

changed: The Angular peer range now says what the code needs. It asked for >=21.1.2, which nothing in this package justified: the newest Angular API the source uses is the signal queries, which shipped in 17.2, and the compiled output carries no linker marker above it. The range is >=17.2.0, and npm run peers:floors derives and checks that floor from now on.

Version 22.10.3 - 9/20/26, 12:00 AM

fixed: The version banner at the top of both READMEs announced 22.10.1 while the manifest published 22.10.2. npm renders the README of each version on its package page, so the banner is what a reader sees before anything else, and it named a release they were not looking at.

Version 22.10.2 - 9/20/26, 12:00 AM

changed: The npm keywords declare ng-hub-ui, the family name somebody searching for the ecosystem types. Metadata only: no code, types or styles change.

Version 22.10.1 - 9/16/26, 12:00 AM

changed: Repository, issue and README links follow the move to the hub-env organization. Issues for every Hub UI package are now gathered in hub-env/hub-ui, and the repository and bugs fields of the manifest point at the new addresses. No code, types or styles change.

fixed: The version banner at the top of both READMEs said the package targets Angular 21. It targets Angular 22, as its major version says.

Version 22.10.0 - 9/8/26, 12:00 AM

added: hubStepTrigger finally draws something. The directive was exported, listed in the module and documented in both READMEs, and the stepper never queried it, so an ng-template marked with it was captured and then dropped on the floor. It now replaces the trigger the default rail draws, once per step, with the step itself as $implicit and title, index, isCurrent, isCompleted and disabled in the context. hubStepperNav still wins where both are present, because a custom rail draws its own triggers.

changed: BREAKING. The CSS block is hub-stepper, and the bare stepper goes in 23.0.0. The host wore the class stepper and named every part under it, including three global keyframes called stepper-fade-in and friends, and emulated encapsulation does not scope keyframe names. stepper is a word in the application namespace, not the library one: a host with a .stepper rule of its own restyled the component from outside. No class is removed in this release, both spellings are written to the DOM and both are matched by the stylesheet, but the old ones are deprecated and go in 23.0.0. The three keyframe names are the exception and are renamed outright, with no alias, because they were never named in the README or the API tables. BREAKING_CHANGES.md has the full rename list.

fixed: A control directive buried inside a step no longer makes the stepper render no control at all. previousButton, nextButton and submitButton are content queries and ran with the default descendants: true, while ng-content select="button[nextButton]" matches only a direct child. So a button nextButton inside a step form was found by the query and could not be projected: the component believed a custom control had been supplied and drew nothing, leaving a wizard with no way forward. The three queries are now shallow, which is exactly what projection can reach.

fixed: The roving focus of the rail is scoped to the rail and finds a custom trigger. It queried .stepper__nav-trigger across the whole host, which would have picked up the tabs of a stepper nested inside a step panel, and which finds nothing once hubStepTrigger draws the trigger. It now searches inside the nav only, and accepts role="tab" as well as either spelling of the class.

Version 22.9.0 - 9/7/26, 12:00 AM

added: provideHubStepper(config?: StepperConfig), the standalone entry point the rest of the family already has. StepperModule.forRoot() was the only thing that registered the bundled en, es, ca, eu, gl, ast, an, de, zh and ar dictionaries and the HubTranslationService the built-in controls resolve their text through, and the module goes in 23.0.0 — so anyone who dropped it lost the ten languages and had to rewrite them, or got a NullInjectorError at first render. The new function registers exactly the same providers without a module; forRoot() now delegates to it, so the two cannot drift apart.

added: STEPPER_DICTIONARIES, the ten bundled dictionaries exported as a plain record. An application that keeps a single translation configuration can now register only the languages it ships, or merge the stepper labels into a dictionary of its own, instead of retyping thirty strings. The keys stay flat — BACK, CONTINUE and SUBMIT — because that is what the component resolves once its HUBUI.STEPPER namespace misses.

changed: The 23.0.0 migration note for StepperModule.forRoot() now points at provideHubStepper() instead of sending the reader off to rewrite ten dictionaries by hand. Nothing changes at runtime; what changes is that the removal announced in 22.8.2 finally has a replacement to name.

Version 22.8.2 - 9/6/26, 12:00 AM

added: Unit specs for the seven building blocks that had none: StepComponent, the nextButton, previousButton and submitButton directives, StepperNavDirective, StepTriggerDirective and StepperThemeService. Coverage stopped at StepperComponent, so every regression in the pieces around it reached a release: an input signal compared instead of read left the projected controls permanently disabled, and a selector written with a descendant combinator stopped matching altogether. The suite now catches both kinds of break on its own, instead of waiting for someone to click through the documentation page.

fixed: Projected nextButton and previousButton controls no longer arrive permanently disabled. Both directives compared the adjacent step disabled input signal itself instead of reading its value, so the expression was a function and therefore always truthy whenever an adjacent step existed. Every custom navigation button a consumer projected was inert and only the built-in controls worked, which is the opposite of what the directives exist for.

fixed: StepComponent.isAccessible() now answers for the step it is asked about. It negated the disabled input signal rather than its value, so it returned false for every step, enabled or not, contradicting the contract its own documentation states.

fixed: The variants "secondary", "neutral", "light" and "dark" are resolved by the stylesheet like the other five built-ins. The component built-in list had stayed at the five variants that predate 22.2.0, so the four added then took the custom-accent branch and were written as an inline style, which outranks both a consumer rule and the hub-stepper-theme() mixin. The rendered colour was already correct; what changes is that overriding the accent now behaves identically for all nine documented variants.

removed: The console.warn for a step declared without a title and the console.error for an out-of-range goTo() are gone. Neither was guarded, so both shipped inside the published bundle and wrote into the console of every consuming application, the first once per untitled step on every render pass that created one. A library has no business logging into the console of its host. Navigation is unchanged: goTo() still ignores an index outside the steps collection.

deprecated: StepperModule and StepperModule.forRoot() are deprecated and will be removed in 23.0.0. The module only re-exports the seven standalone building blocks, so importing them directly is the whole migration. forRoot() needs a word more: it is the only thing that registers the bundled en, es, ca, eu, gl, ast, an, de, zh and ar dictionaries, and those dictionaries are not part of the public API. A standalone application names the three built-in controls through the backLabel, continueLabel and submitLabel inputs of hub-stepper, or registers its own dictionary under HUBUI.STEPPER with provideHubTranslation() from ng-hub-ui-utils, which is also what supplies the HubTranslationService the translate pipe injects. See BREAKING_CHANGES.md.

Version 22.8.1 - 9/1/26, 12:00 AM

changed: The homepage in the manifest points at this library's own documentation page rather than at the site root. It is the link a registry shows beside the package and the one a reader clicks from it, and landing on a front page they then have to search is a worse answer than landing on the reference for the package they were already looking at. Metadata only — no code, no types, no styles change, and nothing a consumer imports is affected.

Version 22.8.0 - 8/14/26, 12:00 AM

changed: Default action labels now resolve HUBUI.STEPPER.* before the legacy flat keys. The component provides the namespace through HUB_TRANSLATION_PREFIX, so an application dictionary can feed the built-in navigation labels via provideHubTranslationAdapter() without reserving generic top-level keys. Existing flat dictionaries keep working — the bare key is still the fallback.

added: README documentation for the application-wide translation adapter (provideHubTranslationAdapter() from ng-hub-ui-utils).

removed: Removed the @angular/animations peer dependency. The package is deprecated upstream and the library never used it. Applications that installed it only for ng-hub-ui-stepper can drop it.

Version 22.7.1 - 8/8/26, 12:00 AM

fixed: Documentation links now point at the canonical localized URLs. The README linked to https://hubui.dev/<path> with no locale prefix and no trailing slash, and both forms are 301-redirected, so every reader arriving from npm or GitHub landed on a redirect instead of the canonical page.

Version 22.7.0 - 7/29/26, 12:00 AM

added: --hub-stepper-indicator-size — the canonical step-indicator diameter (calc(var(--hub-ref-space-3, 1rem) * 2)). The built-in nav is text-only; this is the published metric custom trigger templates and companion step/lifecycle tracks read, space-derived so a density re-theme moves it with the paddings and font sizes that already scale.

Version 22.6.0 - 7/28/26, 12:00 AM

added: WAI-ARIA tablist semantics on the step rail: the rail is role="tablist" with aria-orientation, each trigger is role="tab" with aria-selected / aria-current="step" / aria-disabled, and each step content is a role="tabpanel" wired to its tab through stable generated ids.

added: Keyboard navigation on the rail with a roving tabindex (single Tab stop): Arrow keys move between enabled tabs (skipping disabled ones, wrapping), Home/End jump to the first/last enabled tab, and Enter/Space activates the focused step under the same permission model as clicking.

added: New railLabel input: accessible name of the step rail tablist (default 'Steps').

fixed: Default rail triggers now declare type="button", so a stepper rendered inside a <form> no longer submits it when a step trigger is clicked.

Version 22.5.1 - 7/26/26, 12:00 AM

fixed: StepTriggerDirective selector used a descendant combinator and never instantiated; it now matches either attribute and captures its TemplateRef (groundwork — the stepper does not render custom triggers yet).

changed: Declared the real ng-hub-ui-utils peer range (>=22.7.0); the previous >=1.2.0 floor allowed installs that fail at runtime.

Version 22.5.0 - 7/7/26, 12:00 AM

changed: BREAKING (packaging) — the SCSS now builds to dist/stepper/styles/… instead of dist/stepper/src/lib/styles/…, so @use 'ng-hub-ui-stepper/styles' resolves. Update any @use that reached into src/lib/styles.

changed: The variant input accepts any colour. On top of the built-in semantic accents it now takes a registered custom accent or a literal colour (#ff0000, rgb(…), oklch(…), a CSS named colour), resolved through resolveHubAccent from ng-hub-ui-utils: a bareword becomes var(--hub-sys-color-<name>, <name>) and a literal is used as is.

changed: Internal — host bindings moved from the @HostBinding / @HostListener decorators to the host metadata object, per the Angular style guide. No public API or behaviour change.

Version 22.4.0 - 7/6/26, 12:00 AM

removed: BREAKING — the legacy ng80-stepper / ng80-step element selectors are gone. The components match only hub-stepper / hub-ui-stepper and hub-step / hub-ui-step; replace any <ng80-stepper> / <ng80-step> markup. The major stays at 22 because it tracks the supported Angular major, so this breaking removal ships as a minor.

fixed: CSS variable fallbacks realigned to the ds light defaults (--hub-sys-color-primary: #009ef7 → #0d6efd). Fallbacks only apply when ng-hub-ui-ds is not loaded.

fixed: The default values in docs/css-variables-reference.md and the README theming example were resynchronized with the actual declarations.

Version 22.3.0 - 6/30/26, 12:00 AM

added: New truncateTitles input: each rail title is clipped to --hub-stepper-nav-title-max-width (default 12rem) and reveals its full text on hover when it overflows. The tooltip is agnostic — the hub-ui one by default, swappable with provideHubTooltip. Off by default. Requires ng-hub-ui-utils >= 22.6.0 and @use 'ng-hub-ui-utils/styles/tooltip'.

Version 22.2.0 - 6/26/26, 12:00 AM

added: Open-set accent variants. variant now covers primary, secondary, success, danger, warning, info, neutral, light and dark out of the box, and any other name works with no recompile — define a single --hub-sys-color-<name> and the stepper derives its treatment from it.

added: Derived accent roles --hub-stepper-accent-emphasis, --hub-stepper-accent-subtle and --hub-stepper-accent-on, mixed locally from the single --hub-stepper-accent slot. The active pill and the next / submit controls take their text colour from the on-accent role instead of a hardcoded white, so a light custom accent stays legible.

changed: The accent role family is mixed in the OKLCH colour space for perceptually even tints across every accent, and hub-stepper-theme() re-derives it whenever $accent is passed.

Version 22.1.1 - 6/25/26, 12:00 AM

fixed: Design-token consistency pass: inline fallbacks aligned with the canonical ng-hub-ui-ds values, and hardcoded literals (z-index, font-weight, line-height, radii, theme-aware colours) routed through their --hub-sys-* / --hub-ref-* tokens so they follow the active theme. No visual change when the ds tokens are loaded.

Version 22.1.0 - 6/24/26, 12:00 AM

added: New variant input on <hub-stepper> selecting a semantic accent, and the new --hub-stepper-accent token it re-bases. Defaults to primary, so nothing changes visually until you set it.

added: New hub-stepper-theme() Sass mixin: accent, surfaces, nav pills, controls, spacing and the sidebar width in one call. Every parameter is optional, so only the ones you pass are emitted as --hub-stepper-* overrides. The styles/ folder now ships inside the package.

changed: BREAKING — the --hub-stepper-content-padding and --hub-stepper-nav-padding shorthands were replaced by the canonical directional -padding-x / -padding-y tokens. No visual change; set the directional pair instead of the removed shorthand.

Version 22.0.0 - 6/17/26, 12:00 AM

changed: Aligned with Angular 22 and standardized the README documentation.

Version 21.2.1 - 6/13/26, 12:00 AM

fixed: StepperThemeService.setTheme no longer reaches for the global document, which threw ReferenceError: document is not defined during server-side rendering. It injects the DOCUMENT token instead, making runtime theming SSR-safe.

Version 21.2.0 - 3/19/26, 12:00 AM

added: StepperAnimationDirection enum exported for typed animation direction values.

added: Step indexes are now automatically assigned by StepperComponent via a reactive effect — no manual [index] binding required.

changed: StepComponent.index is now an internal writable signal. Remove all [index]="N" bindings from hub-step templates.

changed: StepComponent.disabled simplified to a direct signal input (input(false)), removing the getter/setter and disabled$ backing signal.

changed: StepperComponent.currentIndex is now a writable signal — call as currentIndex() instead of the previous getter.

changed: Renamed internal signals: removed $ suffix from currentIndex, contentAnimating, animationDirection.

removed: StepComponent.disabled$ backing signal removed. Use the disabled signal input directly.

removed: [index] template binding removed from StepComponent — managed internally.

Version 21.1.0 - 3/18/26, 12:00 AM

added: Angular 21 support with Signals-based architecture.

added: contentChild and contentChildren for step and directive discovery.

changed: All components and directives converted to standalone.

fixed: Fixed step indices and validation during dynamic step additions.

Frequently asked questions

How do I build a multi-step form wizard in Angular?

Install ng-hub-ui-stepper, wrap the steps in <hub-stepper> and declare each one as <hub-step title="...">. Only the current step's content is in the DOM — it renders into a single role="tabpanel" — and the rail of step titles is generated from the projected steps. Add provideHubStepper() to your providers: without it the first render throws, because the translation service behind the button labels is not provided in root.

How do I stop the user advancing until the form is valid?

You gate it yourself. The stepper never inspects your forms: canNavigateTo() answers on the step's own [disabled] input and nothing else, so bind [disabled] to something like a control group's invalid state. Note that goTo() only checks the bounds, so a programmatic jump can still land on a disabled step.

How do I drive the stepper from my own buttons?

Two ways. Put previousButton, nextButton or submitButton on your own <button> elements and the directives wire up the click and the disabled state — those queries deliberately ignore descendants, so the button has to sit outside the <hub-step>, not inside it. Or take a template reference, <hub-stepper #wizard>, and call wizard.goTo(i), goToNext(), goToPrevious() or complete(); currentIndex is a writable signal you can read.

How does it differ from Angular Material's MatStepper?

Validation is the big one: MatStep takes a stepControl and refuses to advance on its own, while this stepper delegates that entirely to your [disabled] binding, and there are no per-step completed, editable, optional or errorState inputs. In exchange, theming is --hub-stepper-* custom properties, also settable at runtime through StepperThemeService.setTheme(); transitions are plain CSS with no @angular/animations dependency; and the whole rail can be replaced with an <ng-template hubStepperNav> or each trigger with hubStepTrigger — at the cost of the built-in tablist and arrow-key handling.