The CDK stepper from @angular/cdk/stepper is an unstyled primitive: the cdkStepper directive owns selection state, each cdk-step declares a label (and optionally a stepControl form), and the consumer renders the headers + body manually. [linear]="true" gates progress on each step's form validity; cdkStepperNext / cdkStepperPrevious advance the selection programmatically.
The four cells of the demo all use the same cdkStepper wiring — only [linear] and orientation change. Header / body layout is consumer-owned; switch between the two by templating the headers in a row (horizontal) vs. interleaving each header with its body (vertical).
@angular/cdk/stepper is an unstyled primitive. See the Angular CDK stepper documentation for the full API. The four examples below cover the cross of linear (forms gate Next) and orientation (horizontal vs. vertical). Reactive-form wizard. Next stays disabled until the current step's form is valid.
Reactive-form checkout flow. Same gating, vertical layout — headers stack and the active step's body renders below its header.
Free navigation — click any header to jump to that step. No form validation gates progress.
Settings-style panel: click any section header to expand its content; no required order.
<!-- Linear ⇒ Next is disabled until the active step's form is valid.
orientation flips the header layout horizontal ↔ vertical. -->
<!-- Linear · Horizontal — signup wizard, gated by form validity. -->
<div cdkStepper [linear]="true" orientation="horizontal">…</div>
<!-- Linear · Vertical — checkout flow, each step body renders inline
under its own header. -->
<div cdkStepper [linear]="true" orientation="vertical">…</div>
<!-- Non-linear · Horizontal — free navigation; users may skip steps. -->
<div cdkStepper [linear]="false" orientation="horizontal">…</div>
<!-- Non-linear · Vertical — settings panel; click any header to expand. -->
<div cdkStepper [linear]="false" orientation="vertical">…</div>
<div cdkStepper #stepper="cdkStepper" [linear]="true" orientation="horizontal">
<cdk-step [stepControl]="accountForm" label="Account">
<form [formGroup]="accountForm">
<input formControlName="name" placeholder="Name">
<input formControlName="email" placeholder="Email" type="email">
</form>
</cdk-step>
<cdk-step label="Review">
<p>Confirm your details and submit.</p>
</cdk-step>
<div class="step-headers">
@for (step of stepper.steps; track $index) {
<button cdkStepHeader type="button"
[disabled]="stepper.linear && $index > stepper.selectedIndex && !step.completed"
(click)="stepper.selectedIndex = $index">
{{ step.label }}
</button>
}
</div>
<div class="step-body">
@if (stepper.selected) {
<ng-container [ngTemplateOutlet]="stepper.selected.content"></ng-container>
}
</div>
<button cdkStepperPrevious [color]="colors.secondary">Back</button>
<button cdkStepperNext [color]="colors.primary">Next</button>
</div>
import { Component } from '@angular/core';
import { CdkStepperModule } from '@angular/cdk/stepper';
import { NgTemplateOutlet } from '@angular/common';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Color } from '@mintplayer/ng-bootstrap';
import { BsButtonTypeDirective } from '@mintplayer/ng-bootstrap/button-type';
@Component({
selector: 'my-stepper-demo',
templateUrl: './my-stepper-demo.component.html',
imports: [CdkStepperModule, NgTemplateOutlet, ReactiveFormsModule, BsButtonTypeDirective],
})
export class MyStepperDemoComponent {
protected readonly colors = Color;
accountForm = new FormGroup({
name: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
email: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.email] }),
});
}