Setting Up Angular CDK
To begin, we'll bring in the Angular CDK using the ng add command, which handles installation through your default package manager.
ng add @angular/cdk
Any package manager of your choosing works for installing the
@angular/cdkpackage —ng addsimply streamlines the process.
After installation, we bring the CdkStepperModule into our application module by importing it from @angular/cdk/stepper.
import { CdkStepperModule } from '@angular/cdk/stepper';
// other imports here
…
@NgModule({
declarations: [AppComponent],
imports: [
…
CdkStepperModule,
…
],
providers: [],
bootstrap: [AppComponent],
})
…
Creating the Stepper Component
Let's generate a fresh component—we'll call it stepper-component for clarity.
ng g c stepper/stepper-component
Component Class
Our new Stepper Component will extend the CdkStepper class. By doing this, our component gains access to all the essential properties and methods that CdkStepper offers, which are fundamental for the stepper's core functionality.
Keep in mind that we're inheriting only the class logic, not the HTML template that comes with CdkStepper:
@Component({
selector: 'app-my-stepper',
templateUrl: './my-stepper.component.html',
styleUrls: ['./my-stepper.component.scss'],
})
export class MyStepperComponent extends CdkStepper {
...
}
Feel free to add your own custom properties to the stepper component as well. This is particularly useful for adjusting the appearance when the stepper appears in different parts of your application. For example, you could introduce an activeClass property to change the CSS class applied to the current step tab:
export class MyStepperComponent extends CdkStepper {
@Input()
activeClass = 'active';
}
Registering the Component
Our custom stepper component needs to declare itself as a CdkStepper in its providers. This step is crucial for other components in the app to identify and interact with our stepper as a standard CdkStepper:
@Component({
selector: 'app-my-stepper',
templateUrl: './my-stepper.component.html',
styleUrls: ['./my-stepper.component.scss'],
providers: [{ provide: CdkStepper, useExisting: MyStepperComponent }],
})
export class MyStepperComponent extends CdkStepper {
// rest of code here
}
Template Structure
Our stepper's template will be split into two main sections: a header for navigation and a body for displaying content.
Stepper Header
The header acts as the navigation area, displaying all available steps and marking the current one. To build this, we'll iterate over the list of steps and use their labels for the header text.
Properties like steps that aren't explicitly defined in our component are inherited directly from the base CdkStepper class:
<header class="header">
<ol>
<ng-container *ngFor="let step of steps; let i = index;">
<li>
<a >
<!-- label here -->
</a>
</li>
</ng-container>
</ol>
</header>
When it comes to defining a label for each step, the CDK Stepper provides two options:
- a simple
labelproperty for plain text labels, or - the
cdkStepLabeldirective, which allows you to use a template for richer labels that might include icons or custom styling. Using this directive is straightforward—just add it to any template element within a step:
<cdk-step>
<ng-template cdkStepLabel>
<!-- Label Content Here -->
</ng-template>
<!-- Step content here -->
</cdk-step>
Here's how a label looks with the cdkStepLabel directive
For maximum flexibility, we'll support both labeling methods in our stepper component. However, when both are present, we'll give the stepLabel directive precedence since a template can convey more information. To implement this, we'll first check if the step's stepLabel property exists. If it does, we render its content using ngTemplateOutlet; otherwise, we fall back to the step's label:
<ng-container *ngIf="step.stepLabel; else showLabelText" [ngTemplateOutlet]="step.stepLabel.template">
</ng-container>
<ng-template #showLabelText>
{{ step.label }}
</ng-template>
To visually differentiate the active step, we compare the current step index with the loop's iteration index in our template.
<li [ngClass]="{'active': selectedIndex === i}">
</li>
Additionally, we want users to jump to a specific step by clicking its label in the header. This is easily done by assigning the clicked step's index to the selected index.
<a (click)="selectedIndex = i">
<!-- label here -->
</a>
Stepper Body
The body section is where the content of the currently selected step is displayed. First, we'll create a wrapper div to contain our body, which also helps with applying styles.
<div > <!-- Add your styling here -->
</div>
Within this container, we'll project the content from the active step. The ngTemplateOutlet directive comes in handy here to seamlessly embed the content associated with the selected step.
<ng-container [ngTemplateOutlet]="selected.content">
</ng-container>
For the sake of readability, I've removed any class names and icons from the code in this section.
Implementing the Stepper in Your App
Now that our stepper component is built, it's ready to be used. Simply include its tag within another component's template:
<app-my-stepper #cdkStepper>
<!-- steps in here -->
</app-my-stepper>
We've assigned a template reference variable #cdkStepper to our stepper component. This reference allows us to easily access the stepper instance from anywhere in the template. For more advanced control, we can use the ViewChild decorator to grab the stepper instance in our component class and store it in a property:
@ViewChild('cdkStepper')
cdkStepper: CdkStepper;
With this property in place, we can now control the stepper programmatically. For instance, calling a method to go to the next step is as simple as:
this.cdkStepper.next()
The CDKStepper class is equipped with several useful optional properties. Let's highlight a few key ones:
linear (boolean)– when set to true, all previous steps must be completed (e.g., valid form inputs) before allowing progression to the next.selected (cdkStep)– defines the currently selected step.selectedIndex (number)– provides an alternative way to select a step by its numerical index.selectionChange (method)– an event that emits whenever the selected step changes.
For a complete list of properties, refer to the official API documentation. Also, don't forget to include any custom properties you've defined for your stepper.
Next, we'll populate our stepper with steps in the template:
<app-my-stepper #cdkStepper>
<cdk-step >
<!-- content here -->
</cdk-step>
</app-my-stepper>
For a detailed list of properties for cdk-step, the API Reference is your best resource. However, here are some important ones to get you started:
stepControl – binds a form control to the step for validation. This only works if the stepper has the linear property enabled, ensuring the form is valid before moving forward.
editable – setting this to false locks the step, preventing users from navigating back to it after they've progressed.
optional – marks a step as optional, meaning it doesn't need to be completed to proceed. This pairs well with the linear mode.
Navigation Buttons
The CDK offers cdkStepperNext and cdkStepperPrevious directives. Adding these to your buttons instantly equips them with the logic to navigate stepper forward and backward:
<!-- Previous Button -->
<button cdkStepperPrevious>
Back
</button>
<!-- Next Button -->
<button cdkStepperNext>
Next
</button>
If you need more control, such as running some logic before navigation, you can manipulate the stepper programmatically using the cdkStepper template reference we defined earlier:
<button (click)="cdkStepper.next()">
Next
</button>
<button (click)="cdkStepper.previous()">
Previous
</button>
Exploring Label Options
As discussed, labels can be added in two ways. The first uses the label property, which works well for straightforward text labels.
<cdk-step label="Personal Details" [stepControl]="frmDetails" [optional]="false">
<!-- content here -->
</cdk-step>
The second, more powerful method uses the cdkStepLabel directive with a template. This opens doors for including icons, custom styling, or any other HTML elements within your labels:
<ng-template cdkStepLabel>
<span class="icon is-medium">
<fa-icon [icon]="faPerson" size="fa-lg"></fa-icon>
</span>
<span>Personal Details</span>
</ng-template>
Integrating Forms with Our Stepper
Now that we've covered building and using a custom stepper, let's dive into how we can combine our stepper with Angular Forms.
First things first, make sure ReactiveFormsModule is included in your module's imports:
// other imports
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [
// ...
],
imports: [
// ...
ReactiveFormsModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Single Form Across All Steps
In this scenario, we manage all steps' data within one overarching form. The strategy involves using a FormArray. Each element in this array is a FormGroup representing an individual step. For example, a stepper with three steps would have a FormArray containing three FormGroup instances. The FormArray itself becomes a field in the parent form that wraps the entire stepper. This setup also allows for applying validators to enforce validation rules. Finally, on the last step, we can submit the entire form.
Component Class Setup
First, we'll declare our main form, frmStepper, as a property of the component:
frmStepper: FormGroup;
Within this main form group, we define a field named steps, which is a FormArray. Each item within this array is a FormGroup corresponding to a step:
this.frmStepper = this.fb.group({
steps: this.fb.array([
this.fb.group({
// ... form controls for our step
}),
// ... more form groups for each step we have
]),
});
Template Adjustments
In the template, we'll wrap our stepper component with a <form> element. We'll also apply the formArrayName directive, binding it to our steps field:
<form [formGroup]="frmStepper">
<app-my-stepper formArrayName="steps">
<!-- cdk steps here -->
</app-my-stepper>
</form>
For each step, we utilize the formGroupName directive to treat each FormGroup in the array independently. We use the index of the step as the value for formGroupName. Additionally, we connect stepControl to each step for validation before navigation:
<cdk-step formGroupName="0" [stepControl]="formArray.get([0])">
<!-- content here -->
</cdk-step>
To supply the correct FormGroup to the stepControl property, we've implemented a getter method for the FormArray. This getter returns the specific FormGroup based on the index we pass:
// formArray getter
get formArray(): AbstractControl {
return this.frmStepper.get('steps');
}
You can also conditionally disable the next button by checking the validity of the current step's form group:
<button [disabled]="formArray.get([1]).invalid" type="button" cdkStepperNext>
Next
</button>
Multiple Forms Approach
Alternatively, you might opt for a separate form for each step. This method assigns an independent FormGroup to every step.
Component Class Design
Here, we'll define distinct forms for each step. In our example with three steps — personal details, address, and payment — we would create three individual forms.
frmDetails = this.fb.group({
// ... form fields here
});
frmAddress = this.fb.group({
// ... form fields here
});
frmPayment = this.fb.group({
// ... form fields here
});
Template Modifications
Instead of one large form, each step's content is wrapped in its own <form> element:
<app-my-stepper >
<cdk-step [stepControl]="frmDetails">
<form (ngSubmit)="frmSubmit(frmDetails)" [formGroup]="frmDetails">
<!-- form content here -->
</form>
</cdk-step>
<cdk-step [stepControl]="frmAddress">
<form (ngSubmit)="frmSubmit(frmDetails)" [formGroup]="frmAddress">
<!-- form content here -->
</form>
</cdk-step>
<cdk-step [stepControl]="frmPayment">
<form (ngSubmit)="frmSubmit(frmDetails)" [formGroup]="frmPayment">
<!-- form content here -->
</form>
</cdk-step>
</app-my-stepper>
For the stepControl property, we supply the respective form group. For instance, in the payment step, the stepControl would be set to frmPayment, referencing the form group we defined in the component class:
<cdk-step [stepControl]="frmPayment">
<!-- form here -->
</cdk-step>
Conclusion
Throughout this article, we walked through the process of adding and configuring Angular CDK within an existing Angular project. We also explored how to create a stepper component that seamlessly matches the visual style and behavior of the rest of your app. In addition, we covered how to integrate the stepper in two frequently encountered use cases: one involving a single form and another dealing with multiple forms.
With the component in place, the next step is to move it into a dedicated feature module. This approach proves especially valuable in larger applications utilizing lazy loading, as it makes the stepper accessible across the entire app while still being loaded on demand. To extend its reach even further, you could share the stepper across several projects within a single workspace using a solution such as NX workspaces.
Source Code and Demo
The complete source code is available here, and a live version can be viewed on Stack Blitz.
