Skip to main content
cngx-src documentation

CngxStepper

ComponentPrimaryOnPushNo encapsulationv0.1.0WCAG AA

projects/ui/stepper/stepper.component.ts

Import#

import { CngxStepper } from '@cngx/ui/stepper'

Description#

Stepper organism. Composes CngxStepperPresenter with CngxRovingTabindex and CngxFocusRestore via hostDirectives; forwards activeStepIndex/linear/orientation/commitAction/ commitMode to the presenter. Material twin lives in @cngx/ui/mat-stepper. ARIA attrs are in the computed() graph.

https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-horizontal/three-step-wizard https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-vertical/vertical-sidebar-layout https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-linear/linear-gating-with-completion-checkboxes https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-mobile-collapse/dots-collapse https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-skins/linear-minimal https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-skins/stripe-status-rich https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-skins/path-chevron https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-skins/pill-segment https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-skins/chips https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-skins/breadcrumb https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-skins/all-skins-side-by-side https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-skins/material-theme-coverage https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-skins/linear-minimal-vertical https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-skins/stripe-status-rich-vertical https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-skins/path-chevron-vertical https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-skins/pill-segment-vertical https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-skins/chips-vertical https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-density/flat-steps-auto-density https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-density/density-across-skins https://cngxjs.github.io/cngx/examples/#/ui/stepper/stepper-density/distance-weighted-collapse

Metadata#

Host#

Providers#

CNGX_STEP_PANEL_HOST
useExisting CngxStepper

Dependencies#

CNGX_STEPPER_HOSTinject()presenter

Stepper-host contract; exposed so external <cngx-stepper-count> / bridge consumers can [host]="s.presenter" via #s="cngxStepper".

CngxStepperSwipeNavinject()hostswipeNav

Mobile-swipe navigation host directive (Level-2 composition).

Relationships

Index#

Inputs#

ariaLabel#string | undefined
input()
default undefined, { alias: 'aria-label' }
ariaLabelledBy#string | undefined
input()
default undefined, { alias: 'aria-labelledby' }
connectors#boolean | undefined, unknown
input()

Opt the classic skin into the connector-rail presentation. Off by default, classic-scoped.

input()

Per-instance header-navigation policy. 'none' renders inert label headers (footer-only navigation); 'visited' keeps them as focusable buttons gated by linear. Cascade: Input ?? config ?? 'visited'.

input()
default undefined,
showStepCount#boolean
input()

Opt-in Step N of M caption under the mobile 'dots' row. In the 'text' collapse branch the count IS the indicator and is always rendered; this input only gates the supplemental caption that sits next to the dot row.

default false
input()

Per-instance skin override; flips [data-skin], structure/ARIA unchanged.

Instance Properties#

activeStepId#Signal
Readonly
this.presenter.activeStepId
activeStepIndex#Signal
Readonly
this.presenter.activeStepIndex
announcement#unknown
ProtectedReadonly

Live-region + per-step + group SR phrase builders (Level-2 factory).

createStepperAnnouncementBuilders({
  presenter: this.presenter,
  stepsOnly: this.stepsOnly,
  i18n: this.i18n,
})
config#unknown
ProtectedReadonly
injectStepperConfig()
displayMode#unknown
ProtectedReadonly
createStepperDisplayMode(
  this.config.mobileBreakpoint ?? STEPPER_DEFAULT_MOBILE_BREAKPOINT,
  () => this.config.mobileCollapse,
  inject(DestroyRef),
)
flatSteps#Signal
Readonly
this.presenter.flatSteps
glyphs#unknown
ProtectedReadonly

Default-template glyph source - read by #defaultBadge / #defaultRejection outlets.

CNGX_STEPPER_GLYPHS
groupNavigation#unknown
ProtectedReadonly

Collapsed-group pointer-navigation view. isNavigable drives the --navigable cursor cue; select is the header click handler that enters the branch at its first reachable step. Level-2 factory.

createStepperGroupNavigation({
  presenter: this.presenter,
  isCollapsed: (node) => this.isGroupCollapsed(node),
  navigationEnabled: () => this.resolvedHeaderNavigation() !== 'none',
})
groupSummary#unknown
ProtectedReadonly

Collapsed-group summary view (groupCollapseSummary). Level-2 factory keeps the count/progress/status derivation out of the organism; the template reads groupSummary.text(node) / .showStatus(node) / .srText(node) on collapsed group headers.

createStepperGroupSummary({
  summaryMode: () => this.config.groupCollapseSummary,
  isCollapsed: (node) => this.isGroupCollapsed(node),
})
handleStripKeyDown#unknown
ProtectedReadonly

Strip-scoped arrow-key handler. See createStepperStripKeyboardNav.

createStepperStripKeyboardNav({
  presenter: this.presenter,
  hostElement: this.hostElement,
  flatStepCount: () => this.flatSteps().length,
  stepButtonIdFor: (id) => `${id}-header`,
  // Defers the post-move focus to afterNextRender so arrow-key
  // navigation across a collapsed group boundary lands on the target
  // button after the strip re-renders its node set.
  injector: this.injector,
  // Off in 'none' mode (inert labels) and while a commit is in flight,
  // so arrow-key navigation locks with the headers + footer during an
  // async transition.
  enabled: () => this.resolvedHeaderNavigation() !== 'none' && !this.presenter.busy(),
})
hostAttrs#unknown
ProtectedReadonly

Resolved skin / connectors / mobile-indicator host attrs (Level-2 cascade helper).

createStepperHostAttrs({
  skin: this.skin,
  connectors: this.connectors,
  mobileIndicatorPosition: this.mobileIndicatorPosition,
  config: this.config,
})
i18n#unknown
ProtectedReadonly
injectStepperI18n()
slotContext#unknown
ProtectedReadonly

Per-step predicates + slot-context builders (Level-2 factory).

createStepperSlotContextBuilders({
  presenter: this.presenter,
  stepsOnly: this.stepsOnly,
  i18n: this.i18n,
})
stateView#unknown
ProtectedReadonly

Shared state view - feeds the mobile-collapse text / dots branches, which otherwise rendered only Step N of M (text) or node.state() (dots) and dropped the per-step error the desktop branch shows.

createStepperStateView({
  presenter: this.presenter,
  stepsOnly: this.stepsOnly,
})
statusLabelFor#unknown
Protected
() => {...}
stepsOnly#Signal
ProtectedReadonly
this.presenter.stepsOnly
stripDensity#unknown
ProtectedReadonly

Space-driven density rung for the classic strip, measured off the host element's own width via createStripDensity. 'full' under the 'comfortable' default. Drives [data-density] (the CSS label-degradation ladder) and the auto-vertical flip below.

createStripDensity({
  element: this.hostElement,
  stepCount: () => this.presenter.stepCount(),
  density: () => this.config.density,
  breakpoints: () => this.config.densityBreakpoints ?? STEPPER_DEFAULT_DENSITY_BREAKPOINTS,
  destroyRef: inject(DestroyRef),
})
templates#unknown
ProtectedReadonly

Template cascade (indicator/badge/busySpinner/rejection/stepError/ groupHeader/empty). Resolution: per-instance slot directive → CNGX_STEPPER_CONFIG.templates.<key>null (built-in default).

createStepperTemplateBindings({
  indicatorSlot: this.indicatorSlot,
  badgeSlot: this.badgeSlot,
  busySpinnerSlot: this.busySpinnerSlot,
  rejectionSlot: this.rejectionSlot,
  errorSlot: this.errorSlot,
  groupHeaderSlot: this.groupHeaderSlot,
  emptySlot: this.emptySlot,
  config: this.config,
})
visibleStripNodes#Signal
ProtectedReadonly

Strip projection honouring the focus-driven group-collapse policy. Drives the classic strip @for; collapsed groups contribute their header node alone. Sourced from the presenter, never re-derived here (host-token contract). Panels still render every step via stepsOnly, so collapsed steps keep their panel ids in the DOM.

this.presenter.visibleStripNodes

Methods#

clearLastFailed#void

Clear the presenter's lastFailedIndex. Lets template-ref consumers (#s="cngxStepper") dismiss the rejection decoration without injecting CNGX_STEPPER_HOST.

contentTemplateFor#TemplateRef | null
contentTemplateFor(id: string)
@paramidstring
handleHeaderClick#void
Protected
handleHeaderClick(node: CngxStepNode)
@paramnodeCngxStepNode
headerAriaDisabled#"true" | null
Protected
headerAriaDisabled(node: CngxStepNode)

'true' on a header that cannot be navigated to, null otherwise. Folds two channels into one binding: a disabled step and a linear-unreachable step both read as aria-disabled, so the gate is announced rather than silently ignored. The header stays focusable per the ARIA composite-widget disabled-focusable rule. In 'visited' + linear=false this is byte-identical to the prior node.disabled()-only binding, since canNavigateTo returns true for every enabled step when linear is off.

@paramnodeCngxStepNode
isGroupCollapsed#boolean
Protected
isGroupCollapsed(node: CngxStepNode)

true when a rendered strip group header is folded under the focus-driven policy: groupCollapse === 'expand-active', the group has children, and it does not hold the active step. Drives the --collapsed visual de-emphasis. Reads activeGroupId reactively, never a one-shot write.

The fold is a density optimisation, not a disclosure widget: collapse is focus-driven (no toggle control), so the header carries no aria-expanded - an unsupported pairing on role="group" (ARIA 1.2) that would also imply a false disclosure affordance. A collapsed header does offer a pointer shortcut into the group (see handleGroupHeaderClick), but that selects the group's first step rather than expanding it in place, so it stays navigation, not disclosure. Keyboard users reach the same steps via the documented arrow-key contract, which already steps into a collapsed group.

@paramnodeCngxStepNode
isHeaderReachable#boolean
Protected
isHeaderReachable(node: CngxStepNode)

true when the step header at node may be navigated to right now: structurally reachable (canNavigateTo - not disabled, not linear-blocked) AND not while a commit is in flight (busy()). The busy gate mirrors the footer nav atoms, which disable on !canGo* || busy() - so the strip and the footer lock together during an async transition instead of letting a header click supersede the pending commit. Reads the host contract, never the presenter's private linear-block check.

@paramnodeCngxStepNode
labelTemplateFor#TemplateRef | null
labelTemplateFor(id: string)
@paramidstring
mobileDotAriaLabel#string
Protected
mobileDotAriaLabel(node: CngxStepNode, index: number)

Mobile-dot aria-label, appending the errored status when the dot has an error.

@paramnodeCngxStepNode
@paramindexnumber
mobileDotState#string
Protected
mobileDotState(node: CngxStepNode)

Mobile-dot data-state: unified error (rejection + aggregator) over the raw status.

@paramnodeCngxStepNode
stepDescriptorId#string
Protected
stepDescriptorId(node: CngxStepNode)
@paramnodeCngxStepNode
stepDistanceOf#number
Protected
stepDistanceOf(node: CngxStepNode)

Distance of a step from the active one, published per-step as --cngx-step-distance under density: 'auto'. Drives the distance-weighted label budget so the step furthest from the active one collapses first. flatIndex is -1 for group nodes, but only step nodes carry the binding. Pure derived data - no layout read.

@paramnodeCngxStepNode
stepHeaderId#string
Protected
stepHeaderId(node: CngxStepNode)
@paramnodeCngxStepNode
stepPanelId#string
Protected
stepPanelId(node: CngxStepNode)
@paramnodeCngxStepNode

HostBindings#

BindingExpression
[attr.aria-roledescription]stepperRoleDescription()
[attr.aria-orientation]presenter.orientation()
[attr.data-orientation]presenter.orientation()
[attr.data-density]stripDensity()
[attr.data-density-auto]isDensityAuto() ? '' : null
[attr.data-skin]hostAttrs.resolvedSkin()
[attr.data-connectors]hostAttrs.resolvedConnectors() ? 'true' : null
[attr.data-mobile-indicator-position]hostAttrs.resolvedMobileIndicatorPosition()
[attr.aria-label]resolvedAriaLabel()
[attr.aria-labelledby]ariaLabelledBy()
[attr.aria-busy]isCommitting() ? "true" : null
[class.cngx-stepper]true

HostListeners#

EventHandler
(keydown)keydown()

Stepper / Wizard: UX and Accessibility

A stepper (often called a wizard) takes one long, intimidating task and cuts it into a short sequence of ordered steps. Each step shows a small, self-contained slice of work, the user always knows where they are and what is left, and there are explicit checkpoints to go back and correct mistakes. The pattern earns its keep when a single screen would be too dense to reason about, or when later steps depend on decisions made earlier.

This document is about the pattern, not the CNGX API. It explains when to reach for a stepper, how it differs from tabs, how to make it keyboard- and screen-reader-complete, and the mistakes that show up once the happy path is left behind. For the component inputs and outputs, see the Info and API tabs.

When a stepper is the right tool

Reach for a stepper when all of these hold:

  • The task is long enough that one screen would overwhelm.
  • The steps have a natural order, or later steps genuinely depend on earlier answers.
  • The user benefits from a sense of progress ("step 2 of 4") and from a review checkpoint before anything is committed.

Typical homes: onboarding and account setup, checkout and multi-page application forms, internal tooling that walks an operator through a procedure.

Do not use a stepper when the surface is small enough that plain navigation or a single form does the job. Splitting a two-field form into three steps adds clicks and ceremony without adding clarity. A stepper is also a poor fit when the user wants to jump around freely and the steps are independent of each other; that is a tabs problem, not a wizard problem.

Three common shapes

Variant Behaviour Fits
Linear wizard Steps must be completed in order; you cannot skip ahead Checkout, dependent onboarding
Editable stepper Completed steps stay reachable from the indicator for review and edits Flows with frequent backtracking
Branching wizard Later steps change based on earlier answers Eligibility or intent-driven flows

Stepper versus tabs

This is the distinction people get wrong most often, because both render as a row of labels with one panel visible at a time. The difference is about intent and order, and it drives every other decision (ARIA roles, keyboard model, validation).

- Tabs Stepper / Wizard
Mental model Parallel views of the same thing Sequential stages of one task
Order None; pick any tab, any time Ordered; progress matters
Dependency Tabs are independent Later steps may depend on earlier ones
Completion There is nothing to "finish" There is a final submit / done state
Progress Not communicated Core to the pattern (position, completed, remaining)
Validation Per tab, if at all Often gates moving forward
ARIA role tablist / tab / tabpanel Ordered list of steps (not a tablist); current step marked with aria-current="step"
Keyboard Arrow keys move between tabs immediately Tab/Shift+Tab through controls; Next/Back buttons advance

Rule of thumb: if the user could reasonably do the parts in any order and nothing is ever "submitted", you want tabs. If there is a start, an end, and an order in between, you want a stepper. Do not borrow the tab ARIA roles for a stepper; a wizard is not a tablist, and announcing it as one misleads screen-reader users about how it behaves.

Anatomy

Required parts:

  • Step indicator: shows the current position, what is done, and what remains.
  • Step panel: the fields or tasks for the active step, and nothing from other steps.
  • Back / Next actions: move between steps without losing entered data.

Optional but valuable:

  • Review step: summarises every choice before the final commit, with links back to edit.
  • Save and resume: lets the user leave a long flow and come back without starting over.

UX guidance

  • State the purpose of each step before adding any visual complexity. The user should know what this step is for in one glance.
  • Keep a control, its label, and its outcome in the same visual group. When the status text lives far from the thing it describes, users work harder to connect them.
  • Communicate state explicitly. Never make the user infer "this step is done" or "this one failed" from colour or decoration alone.
  • Do not add steps without justification. Every extra step is another click and another place to abandon the flow.
  • Do not assume one layout serves novices and experts equally; supporting text that helps a first-timer can be noise to a returning power user.
  • Design the full lifecycle up front: empty, loading, and error states share the same container as the default view. Steppers that only handle the happy path fall apart the first time a network call is slow or a validation fails.

Progress indication

The indicator is the spine of the pattern. It should answer three questions at all times: where am I, what have I completed, what is left. Communicate completion and the current position with more than colour (an icon, a checkmark, a text label) so the meaning survives for colour-blind users and in high-contrast modes.

Validation

In a linear flow, validation usually gates the Next action: the user cannot advance until the current step is valid. Surface errors at the moment they block progress, attached to the field that caused them, and announce them (see below) rather than relying on a red border alone. In an editable stepper, also reflect that a previously completed step has become invalid, so the user is not surprised at submit time.

Two error channels

A stepper fails in two distinct ways, and the cue should match the cause. CNGX keeps them on separate channels so they never fight for the same surface. Pick by what failed, not by which API is closer to hand.

Channel What went wrong Sync? Surface
Validation The step's own input is wrong (empty required field, malformed value) Yes, local to the step Indicator/badge state on every skin + a message row below the strip (*cngxStepError)
Commit / async The transition was rejected (the server said no on Next) No, an action resolved to a failure Rolled-back step decorated via *cngxStepRejection + a toast/banner announcement

Validation error: the input is wrong

Synchronous, local, nothing has been sent anywhere. Flag the step with the [error] input:

<!-- string: marks the step errored AND supplies the reason -->
<div cngxStep label="Payment" [error]="cardInvalid() ? 'Card number is invalid' : false"></div>

<!-- boolean: state only, when the step's own fields already show their errors -->
<div cngxStep label="Payment" [error]="cardInvalid()"></div>

A string both marks the step (red indicator/badge on every skin) and renders the reason in a row below the strip; on the text / dot / progress-bar variants it folds into the aggregate line instead. A bare [error]="true" shows state only, with no text - the indicator already carries "this step is broken", so there is no point repeating "errored" as prose. For multi-source forms bind [errorAggregator] instead; the same surfaces light up. Theme the message with the *cngxStepError slot:

<cngx-stepper>
  ...
  <ng-template cngxStepError let-message="message">
    <strong>{{ message }}</strong>
  </ng-template>
</cngx-stepper>

Use this channel for required-field checks, format validation, and any "fix this before you continue" gating. It needs no async machinery.

Commit / async error: the transition failed

The step was valid; the operation behind Next was rejected (server-side validation, a save that failed). Wire it through [commitAction]. On rejection the stepper rolls back, marks the offending step with *cngxStepRejection, and you announce the reason through the cngxToastOn / cngxBannerOn bridges - applied straight on the stepper, they read its commit state through DI, so no [state] binding is needed:

<cngx-stepper [commitAction]="submitStep" [commitMode]="'pessimistic'" cngxToastOn cngxBannerOn>
  ...
  <ng-template cngxStepRejection>Could not save - please retry.</ng-template>
</cngx-stepper>

Use this channel for save-on-Next, server-only validation, and optimistic moves that must roll back. commitMode decides whether the user waits on the origin step (pessimistic) or advances eagerly and snaps back on failure (optimistic).

They compose, they never collide

Validation owns *cngxStepError; commit owns *cngxStepRejection. A step can hold a local validation error now and still survive a failed submit later, and each renders on its own surface without overwriting the other. Whichever channel fires, the Accessibility rules below still hold: the message is announced, sits near its step, and is never colour-only.

Accessibility

A11y is not a later audit pass; it is part of the design from the first implementation. Build on semantic HTML first and add ARIA only where the semantics fall short.

Keyboard

The entire flow must be completable with the keyboard alone.

Key Action
Tab Move forward through focusable controls in logical order
Shift+Tab Move backward through focusable controls
Enter / Space Activate the focused button (Next, Back, Submit)

Notes that matter in practice:

  • Focus order stays logical when a panel opens, updates, or reveals new controls. When the user advances a step, move focus to the new panel (its heading or first control) so keyboard and screen-reader users are not stranded on a button that no longer makes sense.
  • Focus stays visible and readable at 200% zoom.
  • Unlike tabs, a stepper does not bind arrow keys to immediate step switching. Steps change through deliberate Next/Back activation, because moving forward may run validation or depend on the current step being complete.

Screen reader

  • Give the stepper itself an accessible name (aria-label or aria-labelledby) so it is not just an unnamed region in the page outline.
  • Mark the current step with aria-current="step", not colour or position alone, so the user's place in the sequence is exposed programmatically.
  • Announce state changes: errors, loading, and completion, using an appropriate live-region politeness. A change the user cannot see must still be heard.
  • Connect labels, hints, and status text to their controls with aria-describedby or with real heading structure, so the relationship is programmatic and not just visual.
  • Keep status regions present in the DOM and toggle their content, rather than adding and removing them, so assistive tech has something stable to observe.

Visual

  • Never encode severity, completion, or selection in colour alone.
  • Test at 200% zoom and with reduced motion enabled.
  • Keep touch targets comfortable for coarse pointers on mobile.

State

  • Keep the canonical state small and derive the rest. A single source of truth for "which step, what is valid, what is entered" avoids forked, contradictory copies.
  • Persist enough context that the user can leave and return without losing progress.
  • Separate the kinds of state: what belongs in the URL, what is a stored preference, and what is transient in-memory interaction state.

Mobile and responsive

  • Keep the pattern stable across common breakpoints; the current step must never get hidden by a layout rearrangement.
  • Avoid fixed heights when step content varies in length.
  • Plan the empty, loading, and error states inside the same container so a slow step does not collapse the layout.

Quick checklist

  • Keyboard-only: can you complete the whole flow with Tab, Shift+Tab, Enter, and Space?
  • One screen-reader pass: are step changes, errors, and completion announced?
  • Does focus move to the new panel on every step change?
  • Is progress (position, done, remaining) communicated without relying on colour?
  • Is the current step marked with aria-current="step" and the stepper itself named?
  • Do empty, loading, and error states share the step container and behave?
  • Does entered data survive going Back and a page refresh?

Structural CSS shared by every stepper skin.
Layout-only - no colors / sizes / borders / fonts beyond what's controlled via --cngx-step-* custom properties. Loaded by <cngx-stepper> via styleUrls. Selectors are class-prefixed (.cngx-stepper__*) and apply globally under encapsulation: None - the cngx-stepper- prefix is the namespace.

State modifiers

On .cngx-stepper__step:

  • [aria-disabled="true"] - cursor + opacity dim (--cngx-step-disabled-opacity)
  • :focus-visible - outline ring via --cngx-step-focus-ring
  • --rejected - persistent-rejection outline + tinted background

Orientation variants

On cngx-stepper:

  • [data-orientation="horizontal"] - default; depth-1 steps and group headers indent with margin-inline-start
  • [data-orientation="vertical"] - grid layout (minmax(8rem, max-content) 1fr); strip becomes a column flex, depth-1 rows use padding-inline-start (combining step padding and group indent)

Slots

  • .cngx-stepper__strip - flex row of steps; column in vertical
  • .cngx-stepper__step - per-step <button> with reset chrome
  • .cngx-stepper__indicator - disc placeholder; painted by skin
  • .cngx-stepper__label - flexible label slot
  • .cngx-stepper__panels - panel host
  • .cngx-stepper__panel - panel; [hidden] collapses
  • .cngx-stepper__group-header - group label row
  • .cngx-stepper__badge - inline error-count disc (--cngx-step-error-badge-color + glyph)
  • .cngx-stepper__rejection-icon - ! disc on rejected steps

All transitions short-circuit under prefers-reduced-motion.


Thematic CSS skin for @cngx/ui/stepper.
Pairs with the structural stepper-base.css (which provides layout, indent, focus ring, panels, error badge, rejection icon) - this file owns indicator state colors, hover affordance, the active disc fill, and the busy spinner.
The host carries .cngx-stepper and renders the __indicator glyph inside each step.

State modifiers

On .cngx-stepper__indicator:

  • [data-state="success"] - completed; success-tinted background
  • [data-state="error"] - errored; danger-tinted background
  • [data-state="disabled"] - dimmed fill via color-mix (5%)

On .cngx-stepper__step:

  • [aria-current="step"] - active; fills the indicator with --cngx-step-active-color (default currentColor)
  • [aria-busy="true"] - pending; pulses the active indicator via cngx-stepper-pulse. Mounts __busy-spinner next to the active step with a 360deg cngx-stepper-spin rotation
  • :hover:not([aria-disabled="true"]) - hover state-layer tint

Both spinner and pulse animations no-op under prefers-reduced-motion.

Slots

  • .cngx-stepper__indicator - state disc with glyph; the disc color flips per data-state and per aria-current / aria-busy
  • .cngx-stepper__busy-spinner - inline-block ring next to the active step, sized via --cngx-step-busy-spinner-size

Inheritance

  • --cngx-step-completed-color -> --cngx-color-success
  • --cngx-step-errored-color -> --cngx-color-danger
  • --cngx-step-pending-color -> --cngx-step-active-color

Pair with

  • @cngx/themes/material/stepper-theme - Material 3 step icon palette

Index#

Skin / linear-minimal

Layout

--cngx-step-gap#*
Default value 0.5rem

Gap between adjacent steps and between a step's indicator / label / badge slots.

--cngx-step-strip-padding#*
Default value 0.5rem 0

Padding shorthand of the strip container.

--cngx-step-padding#*
Default value 0.5rem 0.75rem

Padding shorthand of an individual step button.

--cngx-step-padding-inline#*
Default value 0.75rem

Inline-axis padding of the step button - used by vertical orientation to combine with the group indent.

--cngx-step-padding-compact#*
Default value 0.375rem 0.5rem

Tightened panel padding for container-query collapse. Applied when <cngx-stepper> is rendered inside a container narrower than 600px (sidebar / card / dialog) regardless of viewport.

--cngx-step-indicator-size#*
Default value 1.75rem

Diameter of the indicator disc.

--cngx-step-group-indent#*
Default value 0.5rem

Inline-start indent applied to nested steps and group headers.

State / Disabled

--cngx-step-disabled-opacity#<number>
Default value 0.55

Opacity multiplier for disabled steps.

--cngx-step-indicator-disabled-bg#*
Default value color-mix(in srgb, currentColor 5%, transparent)

Background of the disabled indicator disc.

State / Focus

--cngx-step-focus-ring#*
Default value 2px solid oklch(0.66 0.19 50)

Focus-ring outline shorthand. Matches the cngx focus convention from @cngx/core/theming/reset.css - 2px solid primary ember.

--cngx-step-focus-offset#<length>
Default value 3px

Outline offset of the focus ring. Set to 3px so the ring sits outside the indicator disc rather than on its edge - keeps the ring readable against the filled active / completed / errored disc backgrounds.

Typography

--cngx-step-group-font-weight#<number>
Default value 600

Font-weight of the group header.

--cngx-step-indicator-font-size#*
Default value 0.85em

Font-size of the indicator glyph.

--cngx-step-indicator-font-weight#<number>
Default value 600

Font-weight of the indicator glyph.

State / Error

--cngx-step-badge-size#*
Default value 1.25rem

Hit-target diameter of the inline error badge.

--cngx-step-error-badge-color#<color>
Default value oklch(0.45 0.15 25)

Background color of the inline error badge.

--cngx-step-error-badge-text-color#<color>
Default value oklch(1 0 0)

Glyph color inside the error badge.

--cngx-step-badge-font-size#*
Default value 0.75em

Font-size of the error-badge glyph.

--cngx-step-badge-font-weight#<number>
Default value 700

Font-weight of the error-badge glyph.

--cngx-step-errored-color#<color>
Default value oklch(0.45 0.15 25)

Background of the errored indicator disc. Falls back to --cngx-color-danger.

See: [[--cngx-color-danger]]

State / Rejected

--cngx-step-rejection-outline-width#<length>
Default value 1px

Outline width of the persistent-rejection decoration.

--cngx-step-rejection-outline-color#<color>
Default value oklch(0.62 0.15 60)

Outline color of the persistent-rejection decoration.

--cngx-step-rejection-outline-offset#<length>
Default value 2px

Outline offset of the persistent-rejection decoration.

--cngx-step-rejection-bg#<color>
Default value transparent

Background of the rejected step.

--cngx-step-rejection-icon-size#*
Default value 1.25rem

Hit-target diameter of the persistent-rejection icon (! glyph).

--cngx-step-rejection-icon-bg#<color>
Default value oklch(0.62 0.15 60)

Background of the rejection icon disc.

--cngx-step-rejection-icon-color#<color>
Default value oklch(1 0 0)

Glyph color inside the rejection icon disc.

--cngx-step-rejection-icon-font-size#*
Default value 0.75em

Font-size of the rejection icon glyph.

--cngx-step-rejection-icon-font-weight#<number>
Default value 700

Font-weight of the rejection icon glyph.

Layout / Density

--cngx-step-distance#<number>
Default value 0

Distance of a step from the active one (abs(index - active)). Published per-step as data under density: 'auto'; it raises the step's shrink priority so the furthest steps give up their label first. Set and consumed on the step element, so it does not inherit.

--cngx-step-shrink-weight#<number>
Default value 20

Multiplier turning per-step distance into flex-shrink priority under density: 'auto'. The active step shrinks at priority 1, every other step at 1 + distance * weight, so a high weight makes neighbours collapse to number-only before the active label gives any ground.

--cngx-step-collapsed-min#*
Default value 3.25rem

Minimum width of a step under density: 'auto' - the floor a collapsing step shrinks to, kept wide enough to hold the indicator so a number-bearing step never clips its disc.

--cngx-step-collapsed-label-min#*
Default value 2ch

Minimum label width for non-active steps on label-only skins (no number fallback) under density: 'auto', so a collapsing label keeps a readable stub instead of vanishing.

--cngx-step-active-label-min#*
Default value 4rem

Guaranteed minimum label width for the active step, regardless of step count or container width, so the active label stays readable.

--cngx-step-active-label-max#*
Default value 60cqi

Maximum width of the active label, in container-query units, so a very long active label cannot eat the whole strip and squeeze the sequence out. It also bounds how far the strip reflows when navigating between steps. cqi is 1% of the stepper container, so 60cqi caps the active label at 60% of the strip width; the remaining ~40% (minus the active step's own indicator + padding) is left for the other steps.

Group chip

--cngx-stepper-group-chip-padding#*
Default value 0.125rem 0.5rem

Padding shorthand of a group-header chip. inherits: true so a value set on cngx-stepper reaches the chip in the strip.

--cngx-stepper-group-chip-border-width#<length>
Default value 1px

Border width of a group-header chip.

--cngx-stepper-group-chip-radius#<length>
Default value 999px

Corner radius of a group-header chip (pill by default).

--cngx-stepper-group-chip-font-size#*
Default value 0.8125em

Font-size of a group-header chip - smaller than the step labels so it reads as a tag, not a peer step.

--cngx-stepper-group-chip-line-height#<number>
Default value 1.25

Line-height of a group-header chip; drives the chip's height together with the padding.

--cngx-stepper-group-chip-border#*
Default value color-mix(in srgb, currentColor 22%, transparent)

Border color of a resting group-header chip. Defaults to a faint currentColor outline so the chip themes with the strip; the Material bridge maps it to --mat-sys-outline-variant.

--cngx-stepper-group-chip-bg#*
Default value color-mix(in srgb, currentColor 6%, transparent)

Background of a resting group-header chip.

--cngx-stepper-group-chip-color#*
Default value currentColor

Label color of a resting group-header chip. Tracks the inherited text color by default.

--cngx-stepper-group-chip-active-border#*
Default value var(--cngx-color-primary)

Border color of the active-branch chip (the group owning the current step). No initial-value: the universal syntax keeps the property guaranteed-invalid until set, so the CSS var() fallback chain to --cngx-color-primary survives - the same knob that colours the active step. The Material bridge maps it to --mat-sys-primary.

--cngx-stepper-group-chip-active-bg#*
Default value color-mix(in srgb, var(--cngx-color-primary) 14%, transparent)

Background of the active-branch chip. See {@link --cngx-stepper-group-chip-active-border} for why no initial-value is declared.

--cngx-stepper-group-chip-active-color#*
Default value var(--cngx-color-primary)

Label color of the active-branch chip.

Surface

--cngx-step-color#*
Default value currentColor

Default text color inherited by step labels.

--cngx-step-bg#<color>
Default value transparent

Default background of the stepper root.

State / Resting

--cngx-step-indicator-bg#*
Default value color-mix(in srgb, currentColor 10%, transparent)

Background of the indicator disc at rest - soft tint of the inherited text color via color-mix.

--cngx-step-indicator-color#*
Default value currentColor

Glyph color inside the indicator disc.

State / Active

--cngx-step-indicator-active-color#<color>
Default value oklch(1 0 0)

Glyph color used by the active / completed / errored states so the filled disc reads against bright surfaces.

--cngx-step-active-fill#<color>
Default value oklch(0.66 0.19 50)

Resolved fill of the active indicator disc. Defaults to the cngx ember primary (matching --cngx-color-primary). Consumers override via --cngx-step-active-fill directly, or upstream via --cngx-step-active-color (the consuming rule's var() chain threads through both). Baked-in fallback matches the cngx primary so the un-themed disc fits the foundation palette.

See: [[--cngx-step-active-color]]

State / Completed

--cngx-step-completed-color#<color>
Default value oklch(0.5 0.15 145)

Background of the completed indicator disc. Falls back to --cngx-color-success.

See: [[--cngx-color-success]]

State / Hover

--cngx-step-hover-bg#*
Default value color-mix(in srgb, currentColor 8%, transparent)

Background tint applied on hover of an enabled step.

State / Pending

--cngx-step-busy-spinner-size#*
Default value 0.875rem

Diameter of the busy spinner ring next to the active step.

--cngx-step-busy-spinner-color#*
Default value currentColor

Stroke color of the busy spinner ring.

Skin / linear-minimal

--cngx-step-linear-dot-fill#*
Default value oklch(0.66 0.19 50)

Fill of the active dot in the linear-minimal skin. Falls back to the canonical active-fill so Material consumers inherit primary tones without per-skin overrides.

Skin / stripe-status-rich

--cngx-step-status-pill-bg-upcoming#*
Default value color-mix(in srgb, currentColor 6%, transparent)

Background of the status pill on the stripe-status-rich skin when the step is upcoming. Soft-neutral surface tone.

--cngx-step-status-pill-bg-in-progress#*
Default value oklch(0.66 0.19 50)

Background of the status pill while the step is active / in-progress.

--cngx-step-status-pill-bg-done#*
Default value oklch(0.5 0.15 145)

Background of the status pill once the step is completed.

--cngx-step-status-pill-bg-errored#*
Default value oklch(0.45 0.15 25)

Background of the status pill when the step is errored.

--cngx-step-status-pill-color#<color>
Default value oklch(1 0 0)

Text color of the status pill on the active / completed / errored states. Hits the filled pill background.

Skin / path-chevron

--cngx-step-chevron-tile-active#*
Default value oklch(0.66 0.19 50)

Fill of the chevron tile while the step is active on the path-chevron skin.

--cngx-step-chevron-tile-completed#*
Default value color-mix(in srgb, oklch(0.5 0.15 145) 80%, transparent)

Fill of the chevron tile once the step is completed.

--cngx-step-chevron-tile-errored#*
Default value oklch(0.45 0.15 25)

Fill of the chevron tile when the step is errored.

--cngx-step-chevron-tile-upcoming-bg#*
Default value color-mix(in srgb, currentColor 12%, transparent)

Fill of the upcoming chevron tile - soft surface tint so the chevron silhouette stays visible without competing with the active state.

--cngx-step-chevron-tile-color#<color>
Default value oklch(1 0 0)

Text color of the chevron tile in the filled (active / completed / errored) states.

Skin / pill-segment

--cngx-step-pill-radius#<length-percentage>
Default value 999px

Border-radius of the pill-segment skin container - rounds the entire rail rather than per-step.

--cngx-step-pill-segment-rail-bg#*
Default value color-mix(in srgb, currentColor 6%, transparent)

Rail background of the pill-segment skin - soft surface tint the active card floats on top of. Inherits so the host-set value reaches the strip descendant that paints it.

--cngx-step-pill-segment-fill-active#*
Default value oklch(1 0 0)

Active card surface on the pill-segment skin - rendered as a raised tile inside the rail (iOS-style segmented control). Inherits so the host-set value reaches the step descendant that paints it.

--cngx-step-pill-segment-fill-completed#*
Default value transparent

Optional completed-segment tint. Unset by default - the green check disc carries the "done" cue, so the segment stays flat. Set this to wash completed segments with a tint. Inherits so the host-set value reaches the step descendant that paints it.

--cngx-step-pill-segment-card-shadow#*
Default value 0 1px 2px color-mix(in srgb, currentColor 12%, transparent), 0 1px 1px color-mix(in srgb, currentColor 8%, transparent)

Shadow on the active card in the pill-segment skin. Inherits so the host-set value reaches the step descendant that paints it.

Skin / classic

--cngx-step-connector-color#*
Default value color-mix(in srgb, currentColor 35%, transparent)

Muted color of an upcoming connector segment on the classic skin with [connectors] opted-in. Inherits so host-set overrides cascade to the ::before segment painter.

--cngx-step-connector-completed-color#*
Default value oklch(0.5 0.15 145)

Accent color of a completed connector segment on the classic skin with [connectors] opted-in. Falls back to the canonical completed color so Material consumers inherit success tones without per-skin overrides.

--cngx-step-connector-errored-color#*
Default value oklch(0.55 0.18 25)

Errored color of a connector segment whose preceding step is in data-state='error'. Mirrors --cngx-step-errored-color.

--cngx-step-connector-pending-color#*
Default value oklch(0.66 0.19 50)

In-flight color of a connector segment whose preceding step is in data-state='pending'. Mirrors the active-state color so the rail reads as "progressing".

--cngx-step-connector-inset#<length>
Default value 6px

Symmetric breathing room between each disc edge and the connector rail end on the classic skin with [connectors] opted-in. Inherits so host-set overrides cascade.

--cngx-step-connector-width#<length>
Default value 2px

Thickness of the connector segment on the classic skin with [connectors] opted-in. Inherits so host-set overrides cascade.

Skin / chips

--cngx-step-chips-gap#*
Default value 0.5rem

Gap between the standalone pills of the chips skin.

--cngx-step-chips-radius#<length-percentage>
Default value 999px

Corner radius of each pill in the chips skin (default fully round).

--cngx-step-chips-padding-inline#*
Default value 0.875rem

Inline padding inside each pill in the chips skin.

--cngx-step-chips-padding-block#*
Default value 0.4rem

Block padding inside each pill in the chips skin.

--cngx-step-chips-bg#*
Default value color-mix(in srgb, currentColor 8%, transparent)

Inactive pill surface in the chips skin - a soft neutral tint. Inherits so the host-set value reaches the step descendant.

--cngx-step-chips-color#*
Default value color-mix(in srgb, currentColor 65%, transparent)

Inactive pill label colour in the chips skin - muted vs the active fill. Inherits so the host-set value reaches the step descendant.

--cngx-step-chips-color-active#*
Default value oklch(1 0 0)

Active pill label colour in the chips skin - the contrast tone on the filled accent. Inherits so the host-set value reaches the step descendant. The fill itself follows --cngx-color-primary.

--cngx-step-chips-bg-completed#*
Default value color-mix(in srgb, oklch(0.5 0.15 145) 12%, transparent)

Completed chip surface in the chips skin - a soft success tint. Inherits so the host-set value reaches the step descendant.

--cngx-step-chips-color-completed#*
Default value oklch(0.5 0.15 145)

Completed chip label + check colour in the chips skin. Inherits so the host-set value reaches the step descendant.

Skin / breadcrumb

--cngx-step-breadcrumb-gap#*
Default value 0.5rem

Gap between breadcrumb items (and the chevron separator inset) in the breadcrumb skin.

--cngx-step-breadcrumb-radius#<length-percentage>
Default value 6px

Corner radius of the active-item pill in the breadcrumb skin.

--cngx-step-breadcrumb-color#*
Default value color-mix(in srgb, currentColor 48%, transparent)

Upcoming-item label colour in the breadcrumb skin - muted vs the active / completed items. Inherits so the host value reaches the step descendant.

--cngx-step-breadcrumb-color-active#*
Default value color-mix(in srgb, currentColor 92%, transparent)

Active-item label colour in the breadcrumb skin - full strength. Inherits so the host value reaches the step descendant.

--cngx-step-breadcrumb-color-completed#*
Default value color-mix(in srgb, currentColor 82%, transparent)

Completed-item label colour in the breadcrumb skin. Inherits so the host value reaches the step descendant.

--cngx-step-breadcrumb-fill-active#*
Default value color-mix(in srgb, currentColor 9%, transparent)

Active-item pill surface in the breadcrumb skin - the soft tint behind the current breadcrumb label. Inherits so the host value reaches the step descendant.

--cngx-step-breadcrumb-check-color#*
Default value oklch(0.5 0.15 145)

Completed-item check colour in the breadcrumb skin. Inherits so the host value reaches the step descendant.

Material theme coverage across all skins

import { ChangeDetectionStrategy, Component, ViewEncapsulation, signal } from '@angular/core';

import { CngxStep } from '@cngx/common/stepper';
import { CngxStepper } from '@cngx/ui/stepper';

/**
 * Every `CngxStepper` skin rendered against a Material 3 palette.
 *
 * The example's stylesheet builds a real M3 theme in SCSS: `mat.theme`
 * emits the `--mat-sys-*` system tokens, then the published
 * `@cngx/themes/material/stepper-theme` bridge maps every `--cngx-step-*`
 * onto its Material counterpart:
 *
 * ```scss
 * @use '@angular/material' as mat;
 * @use '@cngx/themes/material/stepper-theme' as stepper;
 *
 * $theme: mat.define-theme((color: (theme-type: light, primary: mat.$azure-palette)));
 * html {
 *   @include mat.theme($theme);
 *   @include stepper.theme($theme);
 * }
 * ```
 *
 * So each skin inherits primary / error / surface / tertiary colours from
 * the Material palette with no per-skin overrides and no hand-copied tokens.
 * `ViewEncapsulation.None` lets the global `html` theme and the
 * `:where(cngx-stepper)` bridge rules reach the steppers.
 */
@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  imports: [CngxStepper, CngxStep],
  styleUrl: './skins-coverage.component.scss',
  template: `
    <div class="coverage">
      <section>
        <h3>classic</h3>
        <cngx-stepper [(activeStepIndex)]="active" aria-label="Classic skin">
          <div cngxStep label="Customer" [completed]="active() > 0"></div>
          <div cngxStep label="Payment" [completed]="active() > 1"></div>
          <div cngxStep label="Review"></div>
        </cngx-stepper>
      </section>
      <section>
        <h3>linear-minimal</h3>
        <cngx-stepper [(activeStepIndex)]="active" skin="linear-minimal" aria-label="Linear minimal">
          <div cngxStep label="Customer" [completed]="active() > 0"></div>
          <div cngxStep label="Payment" [completed]="active() > 1"></div>
          <div cngxStep label="Review"></div>
        </cngx-stepper>
      </section>
      <section>
        <h3>stripe-status-rich</h3>
        <cngx-stepper [(activeStepIndex)]="active" skin="stripe-status-rich" aria-label="Stripe status rich">
          <div cngxStep label="Customer" [completed]="active() > 0"></div>
          <div cngxStep label="Payment" [completed]="active() > 1"></div>
          <div cngxStep label="Review"></div>
        </cngx-stepper>
      </section>
      <section>
        <h3>path-chevron</h3>
        <cngx-stepper [(activeStepIndex)]="active" skin="path-chevron" aria-label="Path chevron">
          <div cngxStep label="Customer" [completed]="active() > 0"></div>
          <div cngxStep label="Payment" [completed]="active() > 1"></div>
          <div cngxStep label="Review"></div>
        </cngx-stepper>
      </section>
      <section>
        <h3>pill-segment</h3>
        <cngx-stepper [(activeStepIndex)]="active" skin="pill-segment" aria-label="Pill segment">
          <div cngxStep label="Customer" [completed]="active() > 0"></div>
          <div cngxStep label="Payment" [completed]="active() > 1"></div>
          <div cngxStep label="Review"></div>
        </cngx-stepper>
      </section>
      <section>
        <h3>chips</h3>
        <cngx-stepper [(activeStepIndex)]="active" skin="chips" aria-label="Chips">
          <div cngxStep label="Customer" [completed]="active() > 0"></div>
          <div cngxStep label="Payment" [completed]="active() > 1"></div>
          <div cngxStep label="Review"></div>
        </cngx-stepper>
      </section>
      <section>
        <h3>breadcrumb</h3>
        <cngx-stepper [(activeStepIndex)]="active" skin="breadcrumb" aria-label="Breadcrumb">
          <div cngxStep label="Customer" [completed]="active() > 0"></div>
          <div cngxStep label="Payment" [completed]="active() > 1"></div>
          <div cngxStep label="Review"></div>
        </cngx-stepper>
      </section>
      <div class="coverage__toolbar">
        <button type="button" (click)="prev()">Previous</button>
        <button type="button" (click)="next()">Next</button>
        <span>Active step: {{ active() }}</span>
      </div>
    </div>
  `,
})
export class SkinsCoverageExample {
  protected readonly active = signal(1);

  protected next(): void {
    this.active.update((i) => Math.min(i + 1, 2));
  }

  protected prev(): void {
    this.active.update((i) => Math.max(i - 1, 0));
  }
}
export { SkinsCoverageExample as AppComponent };