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.
A map of a journey: the steps behind, the step now, and the steps still to come.
@pacit/components/stepperpct-stepper, pct-stepNo APG pattern applies, and none is invented — the reasoning is under Accessibility ↓
import { Component, computed, signal } from '@angular/core';
import { PctButton } from '@pacit/components/button';
import { PctStep, PctStepper } from '@pacit/components/stepper';
/** The application owns the journey and hands the map ONE number; the map draws the rest. */
@Component({
selector: 'demo-stepper',
imports: [PctButton, PctStep, PctStepper],
styles: ':host { display: grid; gap: 12px; }',
template: `
<pct-stepper [step]="step()" ariaLabel="Checkout">
<pct-step>Cart</pct-step>
<pct-step>Delivery</pct-step>
<pct-step>Payment</pct-step>
<pct-step>Review</pct-step>
</pct-stepper>
<div>
<button
pctButton
variant="ghost"
size="sm"
[disabled]="step() <= 1"
(click)="move(-1)"
>
Back
</button>
<button
pctButton
variant="ghost"
size="sm"
[disabled]="atEnd()"
(click)="move(1)"
>
Next
</button>
</div>
`,
})
export class StepperDemo {
readonly step = signal(2);
readonly atEnd = computed(() => this.step() >= 4);
move(by: number): void {
this.step.update((at) => at + by);
}
}stepper.demo.ts — the same file runs here and is highlighted under Code
One import per component, and the shortest possible use — the package itself is installed once, on the start page.
import { PctStepper, PctStep } from '@pacit/components/stepper';| Exported | What it is |
|---|---|
PctStepStatetype'done' | 'current' | 'upcoming' | What a step is at this moment of the journey — computed, never set. |
PctSteppercomponentpct-stepper | A stepper: the map of a multi-step journey — "step 2 of 4" above a checkout. |
PctStepcomponentpct-step | One 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>Each example is the component itself, not a screenshot. Open the code under any of them.
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.
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.
| Name | Type | What it does |
|---|---|---|
steprequired | number | Where 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 '' | string | The 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. |
(click), (focus), the form's submit — are the platform's, and two-way models above carry the rest. The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
| Attribute | Bound to | Note |
|---|---|---|
class | pct-stepper | |
role | list | Static 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-label | ariaLabel() || null |
Two ways in, both stable. No class names to depend on.
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.
| Part | Selector | What it is |
|---|---|---|
marker | [data-pct-part="marker"] | the numbered circle of a step |
track | [data-pct-part="track"] | the line between two markers |
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.
| Token | Styles |
|---|---|
--pct-stepper-gap{pct.space.3}8px | Gap between steps. |
--pct-stepper-label-fg-muted{pct.text-muted}#334155 · #e2e8f0 | Text colour of the label when muted. |
--pct-stepper-label-font-weight-current{pct.font.weight.medium}500 | Font weight of the label for the current one. |
--pct-stepper-marker-bg{pct.surface-100}#f1f5f9 · #1e293b | Background of a step's marker. |
--pct-stepper-marker-bg-current{pct.primary}#2563eb · #60a5fa | Background of a step's marker for the current one. |
--pct-stepper-marker-bg-done{pct.primary}#2563eb · #60a5fa | Background of a step's marker for a done one. |
--pct-stepper-marker-border{pct.border}#e2e8f0 · #334155 | Edge colour of a step's marker. |
--pct-stepper-marker-border-current{pct.primary}#2563eb · #60a5fa | Edge colour of a step's marker for the current one. |
--pct-stepper-marker-border-done{pct.primary}#2563eb · #60a5fa | Edge colour of a step's marker for a done one. |
--pct-stepper-marker-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of a step's marker. |
--pct-stepper-marker-fg-current{pct.on-primary}#ffffff · #0f172a | Text colour of a step's marker for the current one. |
--pct-stepper-marker-fg-done{pct.on-primary}#ffffff · #0f172a | Text colour of a step's marker for a done one. |
--pct-stepper-marker-font-size{pct.font.size.sm}13px | Font size of a step's marker. |
--pct-stepper-marker-radius999px | Corner radius of a step's marker. |
--pct-stepper-marker-size28px | Size of a step's marker. |
--pct-stepper-track-bg{pct.border}#e2e8f0 · #334155 | Background of the track. |
--pct-stepper-track-size24px | Size of the track. |
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;
} 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.
| key | effect | test |
|---|
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).
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.
libs/components/stepper/src/stepper.tsapps/sandbox-e2e/src/a11y.spec.ts — /stepper in SBX_ROUTESapps/sandbox-e2e/src/visual.spec.ts — stepper-journey and stepper-journey-rtlforced-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.tsprefers-reduced-motion — duration from a tokennot applicable — no motion at all--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.jsondir="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-rtlNG05xxapps/sandbox-e2e/src/hydration.spec.ts — /stepper in SBX_ROUTESlibs/components/parts.snapshot.md — marker, trackcontrast.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.mdPCT_TEXTSlibs/components/core/src/texts.ts — stepDone, read at render timelibs/components/size.snapshot.md — the ./stepper rowdocs/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 walksapps/sandbox/src/app/views/stepper/ (the sandbox view). The published site: /components/stepper — prerendered, the demo's own source is the code tablibs/components/stepper/src/stepper.spec.ts — 13 cases; libs/components/mutation.snapshot.md — stepper.ts measured in the day's shared full runRead at build time from the tracked snapshots, not typed here.
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).