CngxStepper
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.
Metadata#
Host#
Providers#
CNGX_STEP_PANEL_HOST- useExisting
CngxStepper
Dependencies#
CNGX_STEPPER_HOSTinject()presenterStepper-host contract; exposed so external <cngx-stepper-count> / bridge consumers can [host]="s.presenter" via #s="cngxStepper".
CngxStepperSwipeNavinject()hostswipeNavMobile-swipe navigation host directive (Level-2 composition).
Relationships
Index#
Properties
Methods
Derived State
HostBindings
HostListeners
Inputs#
boolean | undefined, unknownOpt the classic skin into the connector-rail presentation. Off by default, classic-scoped.
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.
falsePer-instance skin override; flips [data-skin], structure/ARIA unchanged.
Instance Properties#
unknownLive-region + per-step + group SR phrase builders (Level-2 factory).
createStepperAnnouncementBuilders({
presenter: this.presenter,
stepsOnly: this.stepsOnly,
i18n: this.i18n,
})unknowncreateStepperDisplayMode(
this.config.mobileBreakpoint ?? STEPPER_DEFAULT_MOBILE_BREAKPOINT,
() => this.config.mobileCollapse,
inject(DestroyRef),
)unknownDefault-template glyph source - read by #defaultBadge / #defaultRejection outlets.
CNGX_STEPPER_GLYPHSunknownCollapsed-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),
})unknownStrip-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(),
})unknownResolved skin / connectors / mobile-indicator host attrs (Level-2 cascade helper).
createStepperHostAttrs({
skin: this.skin,
connectors: this.connectors,
mobileIndicatorPosition: this.mobileIndicatorPosition,
config: this.config,
})unknownPer-step predicates + slot-context builders (Level-2 factory).
createStepperSlotContextBuilders({
presenter: this.presenter,
stepsOnly: this.stepsOnly,
i18n: this.i18n,
})unknownShared 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,
})unknownSpace-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),
})unknownTemplate 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,
})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.visibleStripNodesMethods#
Clear the presenter's lastFailedIndex. Lets template-ref consumers
(#s="cngxStepper") dismiss the rejection decoration without injecting
CNGX_STEPPER_HOST.
"true" | nullheaderAriaDisabled(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.
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.
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.
mobileDotAriaLabel(node: CngxStepNode, index: number)Mobile-dot aria-label, appending the errored status when the dot has an error.
mobileDotState(node: CngxStepNode)Mobile-dot data-state: unified error (rejection + aggregator) over the raw status.
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.
HostBindings#
| Binding | Expression |
|---|---|
[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#
| Event | Handler |
|---|---|
(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-labeloraria-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-describedbyor 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 withmargin-inline-start[data-orientation="vertical"]- grid layout (minmax(8rem, max-content) 1fr); strip becomes a column flex, depth-1 rows usepadding-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 viacolor-mix(5%)
On .cngx-stepper__step:
[aria-current="step"]- active; fills the indicator with--cngx-step-active-color(defaultcurrentColor)[aria-busy="true"]- pending; pulses the active indicator viacngx-stepper-pulse. Mounts__busy-spinnernext to the active step with a 360degcngx-stepper-spinrotation: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 perdata-stateand peraria-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#
Layout
State / Disabled
State / Focus
Typography
State / Error
State / Rejected
Layout / Density
Group chip
State / Resting
State / Completed
State / Hover
State / Pending
Skin / linear-minimal
Skin / stripe-status-rich
Skin / path-chevron
Skin / pill-segment
Skin / classic
Skin / chips
Layout
*0.5remGap between adjacent steps and between a step's indicator / label / badge slots.
*0.75remInline-axis padding of the step button - used by vertical orientation to combine with the group indent.
*0.375rem 0.5remTightened panel padding for container-query collapse. Applied
when <cngx-stepper> is rendered inside a container narrower
than 600px (sidebar / card / dialog) regardless of viewport.
*0.5remInline-start indent applied to nested steps and group headers.
State / Disabled
*color-mix(in srgb, currentColor 5%, transparent)Background of the disabled indicator disc.
State / Focus
*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.
<length>3pxOutline 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
State / Error
<color>oklch(0.45 0.15 25)Background color of the inline error badge.
<color>oklch(1 0 0)Glyph color inside the error badge.
<color>oklch(0.45 0.15 25)Background of the errored indicator disc. Falls back to
--cngx-color-danger.
See: [[--cngx-color-danger]]
State / Rejected
<length>1pxOutline width of the persistent-rejection decoration.
<color>oklch(0.62 0.15 60)Outline color of the persistent-rejection decoration.
<length>2pxOutline offset of the persistent-rejection decoration.
*1.25remHit-target diameter of the persistent-rejection icon (! glyph).
<color>oklch(0.62 0.15 60)Background of the rejection icon disc.
<color>oklch(1 0 0)Glyph color inside the rejection icon disc.
<number>700Font-weight of the rejection icon glyph.
Layout / Density
<number>0Distance 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.
<number>20Multiplier 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.
*3.25remMinimum 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.
*2chMinimum 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.
*4remGuaranteed minimum label width for the active step, regardless of step count or container width, so the active label stays readable.
*60cqiMaximum 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
*0.125rem 0.5remPadding shorthand of a group-header chip. inherits: true so a value
set on cngx-stepper reaches the chip in the strip.
<length>1pxBorder width of a group-header chip.
<length>999pxCorner radius of a group-header chip (pill by default).
*0.8125emFont-size of a group-header chip - smaller than the step labels so it reads as a tag, not a peer step.
<number>1.25Line-height of a group-header chip; drives the chip's height together with the padding.
*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.
*color-mix(in srgb, currentColor 6%, transparent)Background of a resting group-header chip.
*currentColorLabel color of a resting group-header chip. Tracks the inherited text color by default.
*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.
*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.
*var(--cngx-color-primary)Label color of the active-branch chip.
Surface
State / Resting
*color-mix(in srgb, currentColor 10%, transparent)Background of the indicator disc at rest - soft tint of the
inherited text color via color-mix.
State / Active
<color>oklch(1 0 0)Glyph color used by the active / completed / errored states so the filled disc reads against bright surfaces.
<color>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
<color>oklch(0.5 0.15 145)Background of the completed indicator disc. Falls back to
--cngx-color-success.
See: [[--cngx-color-success]]
State / Hover
*color-mix(in srgb, currentColor 8%, transparent)Background tint applied on hover of an enabled step.
State / Pending
*0.875remDiameter of the busy spinner ring next to the active step.
Skin / linear-minimal
*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
*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.
*oklch(0.66 0.19 50)Background of the status pill while the step is active / in-progress.
*oklch(0.5 0.15 145)Background of the status pill once the step is completed.
*oklch(0.45 0.15 25)Background of the status pill when the step is errored.
<color>oklch(1 0 0)Text color of the status pill on the active / completed / errored states. Hits the filled pill background.
Skin / path-chevron
*oklch(0.66 0.19 50)Fill of the chevron tile while the step is active on the
path-chevron skin.
*color-mix(in srgb, oklch(0.5 0.15 145) 80%, transparent)Fill of the chevron tile once the step is completed.
*oklch(0.45 0.15 25)Fill of the chevron tile when the step is errored.
*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.
<color>oklch(1 0 0)Text color of the chevron tile in the filled (active / completed / errored) states.
Skin / pill-segment
<length-percentage>999pxBorder-radius of the pill-segment skin container - rounds the
entire rail rather than per-step.
*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.
*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.
*transparentOptional 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.
*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
*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.
*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.
*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.
*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".
<length>6pxSymmetric 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.
<length>2pxThickness of the connector segment on the classic skin with
[connectors] opted-in. Inherits so host-set overrides cascade.
Skin / chips
<length-percentage>999pxCorner radius of each pill in the chips skin (default fully round).
*0.875remInline padding inside each pill in the chips skin.
*0.4remBlock padding inside each pill in the chips skin.
*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.
*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.
*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.
*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.
*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
*0.5remGap between breadcrumb items (and the chevron separator inset) in
the breadcrumb skin.
<length-percentage>6pxCorner radius of the active-item pill in the breadcrumb skin.
*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.
*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.
*color-mix(in srgb, currentColor 82%, transparent)Completed-item label colour in the breadcrumb skin. Inherits so
the host value reaches the step descendant.
*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.
*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 };