ComponentsData & statusStepper
On this page
Data & status

Stepper released

A map of a journey: the steps behind, the step now, and the steps still to come.

  • Import@pacit/components/stepper
  • Selectorspct-stepper, pct-step

No APG pattern applies, and none is invented — the reasoning is under Accessibility ↓

Cart CompletedDeliveryPaymentReview

stepper.demo.ts — the same file runs here and is highlighted under Code

Start here

Usage

One import per component, and the shortest possible use — the package itself is installed once, on the start page.

Import this component
import { PctStepper, PctStep } from '@pacit/components/stepper';
ExportedWhat it is
PctStepStatetype'done' | 'current' | 'upcoming'What a step is at this moment of the journey — computed, never set.
PctSteppercomponentpct-stepperA stepper: the map of a multi-step journey — "step 2 of 4" above a checkout.
PctStepcomponentpct-stepOne step of the map: role="listitem", the connector and the marker drawn before the projected label, and the state computed from the row's one number — never set here.
<pct-stepper [step]="2" ariaLabel="Checkout">
  <pct-step>Cart</pct-step>
  <pct-step>Delivery</pct-step>
  <pct-step>Payment</pct-step>
</pct-stepper>
Examples

Every state, running

Each example is the component itself, not a screenshot. Open the code under any of them.

A journey the application steers

The stepper is a map, not a wizard: step is one-indexed and the application moves it. Nothing here is clickable on purpose — a step you could jump to would be navigation, and that belongs to the page's own links.

Account CompletedWorkspaceInvite
API

What it reads, and what it writes back

Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.

A stepper: the map of a multi-step journey — "step 2 of 4" above a checkout.

Inputs

NameTypeWhat it does
steprequirednumberWhere the journey stands, 1-based — "step 2 of 4" is what the user reads, and the same number is the current marker's visible ordinal (the pagination's argument for its page). Below 1 every step is upcoming; past the count every step is done — both are honest sentences about a journey not yet started or already finished, so nothing is clamped and nothing written back.
ariaLabeldefault ''stringThe accessible name of the list. Optional, the chips' reasoning: a list is allowed to be nameless, and a library default would have to guess what journey this is — two maps on one page ("Checkout", "Onboarding") are told apart here.

Outputs

None of its own. The native events — (click), (focus), the form's submit — are the platform's, and two-way models above carry the rest.

What it writes on the element

The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.

AttributeBound toNote
classpct-stepper
rolelistStatic for the chips' measured reason: an empty list raises nothing in any engine's audit (lesson-138), so a row with no steps yet needs no machinery.
aria-labelariaLabel() || null
Styling

Parts to select, tokens to override

Two ways in, both stable. No class names to depend on.

Parts

In a test, PctStepperHarness, PctStepHarness from @pacit/components/testing read a part by the name below and a state by its attribute — the harness gate holds the names to the package.

PartSelectorWhat it is
marker[data-pct-part="marker"]the numbered circle of a step
track[data-pct-part="track"]the line between two markers

Tokens

Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.

TokenStyles
--pct-stepper-gap{pct.space.3}8pxGap between steps.
--pct-stepper-label-fg-muted{pct.text-muted}#334155 · #e2e8f0Text colour of the label when muted.
--pct-stepper-label-font-weight-current{pct.font.weight.medium}500Font weight of the label for the current one.
--pct-stepper-marker-bg{pct.surface-100}#f1f5f9 · #1e293bBackground of a step's marker.
--pct-stepper-marker-bg-current{pct.primary}#2563eb · #60a5faBackground of a step's marker for the current one.
--pct-stepper-marker-bg-done{pct.primary}#2563eb · #60a5faBackground of a step's marker for a done one.
--pct-stepper-marker-border{pct.border}#e2e8f0 · #334155Edge colour of a step's marker.
--pct-stepper-marker-border-current{pct.primary}#2563eb · #60a5faEdge colour of a step's marker for the current one.
--pct-stepper-marker-border-done{pct.primary}#2563eb · #60a5faEdge colour of a step's marker for a done one.
--pct-stepper-marker-fg{pct.text-muted}#334155 · #e2e8f0Text colour of a step's marker.
--pct-stepper-marker-fg-current{pct.on-primary}#ffffff · #0f172aText colour of a step's marker for the current one.
--pct-stepper-marker-fg-done{pct.on-primary}#ffffff · #0f172aText colour of a step's marker for a done one.
--pct-stepper-marker-font-size{pct.font.size.sm}13pxFont size of a step's marker.
--pct-stepper-marker-radius999pxCorner radius of a step's marker.
--pct-stepper-marker-size28pxSize of a step's marker.
--pct-stepper-track-bg{pct.border}#e2e8f0 · #334155Background of the track.
--pct-stepper-track-size24pxSize of the track.

Theming

A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.

[data-theme='brand'] {
  --pct-stepper-marker-bg-current: #0f766e;
  --pct-stepper-marker-bg-done: #ccfbf1;
  --pct-stepper-marker-border-done: #0f766e;
}
Cart CompletedDeliveryPaymentReview
Accessibility

Measured, not assumed

Pattern: none — none exists for a stepper, and none is invented (0055): the host is a list, every step a listitem — the geometry the breadcrumb's probe measured clean in three engines — and aria-current="step" stands on the step the input names. Named in the class JSDoc.

Keyboard

keyeffecttest

Empty by design: the map draws no control and takes no focus. A step the application makes pressable brings its own keyboard with the projected element (req-api-platform).

What is measured, and what is not yet

Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.

MeasuredARIA pattern named in the class JSDoclibs/components/stepper/src/stepper.ts
MeasuredKeyboard map tested key by keythe empty map is the claim — nothing here is focusable, and the drawings are hidden from the tree outright
Measuredaxe audit on the component's own sandbox viewapps/sandbox-e2e/src/a11y.spec.ts — /stepper in SBX_ROUTES
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — stepper-journey and stepper-journey-rtl
Measuredforced-colors: active — no state carried by colour alonelibs/components/stepper/src/step.scss — the border keeps the circles, the check keeps done, the weight keeps current. The reading: apps/sandbox-e2e/src/forced-colors.spec.ts
n/aprefers-reduced-motion — duration from a tokennot applicable — no motion at all
n/aTouch target ≥ 24×24 px outrightnot applicable — nothing here is a target; a pressable step is the application's projected control, which brings the floor with it
MeasuredSize axis aligned to --pct-control-height-*deliberately not: the map is not a control; the marker is its own literal with the refusal written beside it in component.stepper.json
GapDensity axisnone — gap: the same one every component here has (req-token-density)
MeasuredRTL — no physical properties + a dir="rtl" screenshotapps/sandbox-e2e/src/rtl.spec.ts — the journey descends the other way, the connector changes sides with no rule to help it (a line is a line both ways). The screenshot: stepper-journey-rtl
MeasuredSSR + hydration with no NG05xxapps/sandbox-e2e/src/hydration.spec.ts — /stepper in SBX_ROUTES
n/aFormsnot applicable — a map holds no value a form owns
MeasuredParts registered in the inventorylibs/components/parts.snapshot.md — marker, track
MeasuredTokens registered + an entry in contrast.policy.jsonlibs/tokens/src/contrast.policy.json — seven entries: three marker pairs (AA), the done fill and the upcoming edge against the page (UI), the muted label (AA), the connector (UI warn); libs/tokens/tokens.snapshot.md
MeasuredStrings through PCT_TEXTSlibs/components/core/src/texts.ts — stepDone, read at render time
MeasuredEntrypoint size budgetlibs/components/size.snapshot.md — the ./stepper row
MeasuredScreen-reader logRead 2026-09-16 from docs/acr/at/ — three readers, whole sandbox, and the steps are not focusable, so a Tab walk does not reach them. The logs hold the two buttons only: Back · button. (Orca), Next, button (NVDA). VoiceOver answered the same stop with its own instruction — You are currently on a button. To click this button, press Control-Option-Space. — which is the reader talking about itself and not about this component. What a reader makes of the step list needs a pass that reads rather than walks
MeasuredA docs page with live examplesapps/sandbox/src/app/views/stepper/ (the sandbox view). The published site: /components/stepper — prerendered, the demo's own source is the code tab
MeasuredUnit + mutationlibs/components/stepper/src/stepper.spec.ts — 13 cases; libs/components/mutation.snapshot.md — stepper.ts measured in the day's shared full run
Evidence

What the repository holds for this component

Read at build time from the tracked snapshots, not typed here.

89.8%mutation score44 of 49 mutants killed
6 × 3e2e cases × enginesplus the shared audits: axe, forced colors, preferences, size, hydration
9colour pairs measuredlight and dark, at every token build
2visual baselinesChromium, the sandbox's own pictures
22elements in the preview2 listeners · 1 render to settle · 5609 µs in jsdom, held exactly but for the clock

Known limitations

  • The map cannot verify the journey. step=3 beside a form whose second page failed validation is the application lying to its users; the component renders the statement and checks only structure (the dev-mode warning).
  • No interactive steps of the component's own. A pressable step is the application's link or button projected into the label — the component cannot know whether going back is allowed, so it draws no control it would then have to guard.
  • No vertical orientation. It returns as its own decision the day a real application asks; a journey too wide for its line wraps.
  • No step content / panels. A wizard's pages are routed views; a stepper that owned panels would be the tabs in a costume.