Building a Custom Stepper using Angular CDK
Learn how to build a stepper component using Angular CDK, just like the one in Angular Material, but with your own look and feel.

Building a Custom Stepper using Angular CDK
Learn how to build a stepper component using Angular CDK, just like the one in Angular Material, but with your own look and feel.


In this article, we will learn how to build a stepper component using Angular CDK, just like the one in Angular Material. While it will function just like the Material Stepper Component, we will be using our theme so we can customize how it looks and feels.
Angular's Component Development Kit (CDK) is a set of tools that implements common interaction patterns while being unopinionated about the UI. It is used as the building block for most angular UI frameworks like Angular Material.
For this article, we will be using Bulma CSS Framework for styling, feel free to replace this with your custom CSS styles or any CSS framework.
Installing & Setting up Angular CDKLink to this section
First, we will use ng add
to install angular CDK using the default package manager.
<>Copyng add @angular/cdk
You can use your favorite package manager to install@angular/cdk
package –ng add
in this case does not do anything.
Then, we will import CdkStepperModule
from @angular/cdk/stepper
in our app module.
<>Copyimport { CdkStepperModule } from '@angular/cdk/stepper'; // other imports here … @NgModule({ declarations: [AppComponent], imports: [ … CdkStepperModule, … ], providers: [], bootstrap: [AppComponent], }) …
Building the Stepper ComponentLink to this section
We will start by generating a new component, which we will creatively name stepper-component.
<>Copyng g c stepper/stepper-component
Component ClassLink to this section
Next, we will extend the CdkStepper
class with our new Stepper Component. This allows our custom stepper component to inherit all the properties and methods from the CdkStepper
class. These methods and properties are necessary to make our stepper component work.
Please note, we will only inherit properties and methods from CdkStepper
and not the template:
<>Copy@Component({ selector: 'app-my-stepper', templateUrl: './my-stepper.component.html', styleUrls: ['./my-stepper.component.scss'], }) export class MyStepperComponent extends CdkStepper { ... }
You can also add custom props to your custom stepper component, just like you would in any other angular component. This gives you the ability to tinker with the stepper component appearance if it is used in multiple places within your app. For instance, if you wanted to override the default current step tab CSS class, you can add an activeClass
props for that, as shown below:
<>Copyexport class MyStepperComponent extends CdkStepper { @Input() activeClass = 'active'; }
Providing the ComponentLink to this section
We will make our custom stepper component provide itself as a CdkStepper
. This allows other components in our angular app to recognize our custom stepper component as a CdkStepper
:
<>Copy@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 }
TemplateLink to this section
We will have two sections for Our stepper component – a header and a body.
Stepper HeaderLink to this section
The header section will be for navigation purposes – showing all the steps we currently have and highlighting the current step. To achieve this, we are going to loop over the steps and use the step labels for the header label.
The steps property and any other properties that are not defined in our component are inherited from the CdkStepper
class:
<>Copy<header class="header"> <ol> <ng-container *ngFor="let step of steps; let i = index;"> <li> <a > <!-- label here --> </a> </li> </ng-container> </ol> </header>
For our header’s label, CDK Stepper supports two ways to provide it:
- a
label
props which is plain text, and - a
cdkStepLabel
directive, a template you can use to add richer labels such as icons and styling. To use thecdkStepLabel
directive, you just need to add the directive to a template, inside a step, as shown below:
<>Copy<cdk-step> <ng-template cdkStepLabel> <!-- Label Content Here --> </ng-template> <!-- Step content here --> </cdk-step>
For this article, we will support both, giving our stepper versatility. Feel free to support just one, nothing is wrong with that. We will give the stepLabel
directive priority though, so if the user provides both, we will display the template version, because it is richer. To support both, we will first check if steps’ stepLabel
property is defined, then display its content using ngTemplateOutlet
and default to steps’ label
property if it is not defined:
<>Copy<ng-container *ngIf="step.stepLabel; else showLabelText" [ngTemplateOutlet]="step.stepLabel.template"> </ng-container> <ng-template #showLabelText> {{ step.label }} </ng-template>
To highlight the current step label, we will check if the current index equals to the current position index of loop of steps.
<>Copy<li [ngClass]="{'active': selectedIndex === i}"> </li>
We will also make it possible for users to navigate via our header by clicking on the label of the step. We can achieve this by setting the selected index as the index of our current step.
<>Copy<a (click)="selectedIndex = i"> <!-- label here --> </a>
Stepper BodyLink to this section
The body will hold the content of the current/selected step. First, we will start by setting a container for our body, for styling purposes.
<>Copy<div > <!-- Add your styling here --> </div>
Then, in our body container, we will project the content of the current step. We will use ngTemplateOutlet to embed the content from the currently selected step.
<>Copy<ng-container [ngTemplateOutlet]="selected.content"> </ng-container>
I have stripped out the class names and icons, to make it easier on the eye.
Using the Stepper ComponentLink to this section
Our stepper component is now ready for use inside our angular app. First, inside another component, just add the tags of our stepper component.
<>Copy<app-my-stepper #cdkStepper> <!-- steps in here --> </app-my-stepper>
For us to be able to refer to the stepper we are creating, we have given it a template reference variable #cdkStepper
. This makes it easy to refer to the stepper anywhere within the component. For instance, we can control the stepper from the components’ class. To achieve this, we will use the ViewChild decorator to query the view for the stepper, and assign it to a property of the component:
<>Copy@ViewChild('cdkStepper') cdkStepper: CdkStepper;
Then, in one of your methods, you can move to the next step, like this:
<>Copythis.cdkStepper.next()
The CDKStepper
class comes with several optional props that you can pass to the custom stepper component. Here are some of the important ones:
linear (boolean)
– requires the last step to be complete before proceeding to the next, i.e. a form has valid inputs.selected (cdkStep)
– the step that is selected.selectedIndex (number)
– the index of the step that is selected, an alternative to the selected input.selectionChange (method)
– an event emitted whenever the selected steps change.
You can find all the props here. Remember to add any custom props you created.
Then, we can add the steps for our stepper component as shown below:
<>Copy<app-my-stepper #cdkStepper> <cdk-step > <!-- content here --> </cdk-step> </app-my-stepper>
For the cdk-step props to use, please refer to the API Reference here, but here are a few notable ones:
stepControl
– provide a form control that can be validated before proceeding to the next step. The linear mode must be enabled on the stepper for this to work.
editable
– when set to false, it prevents the user from navigating back to a step once they have moved to the next step.
optional
– whether the completion of a step is required. It works in tandem with linear mode, so you can have some option steps.
Next and Previous ButtonsLink to this section
CDK Stepper provides two directives – cdkStepperNext
and cdkStepperPrevious
– which you can add to your next and previous buttons to add the ability to navigate forward and backward:
<>Copy<!-- Previous Button --> <button cdkStepperPrevious> Back </button> <!-- Next Button --> <button cdkStepperNext> Next </button>
You can also control the stepper programmatically instead of using the directives. This is great as it gives you the freedom to do something else like saving a form before moving to the next step. This can be achieved by using the template variable cdkStepper
, which we added earlier and refers to our stepper.
<>Copy<button (click)="cdkStepper.next()"> Next </button> <button (click)="cdkStepper.previous()"> Previous </button>
What about Labels?Link to this section
There are two ways of adding labels, the first one is using the label component props. This accepts plain text only labels.
<>Copy<cdk-step label="Personal Details" [stepControl]="frmDetails" [optional]="false"> <!-- content here --> </cdk-step>
While the second one involves using a template with the cdkStepLabel
directive. This approach is more flexible as it allows you to add icons, styling, etc. to your label:
<>Copy<ng-template cdkStepLabel> <span class="icon is-medium"> <fa-icon [icon]="faPerson" size="fa-lg"></fa-icon> </span> <span>Personal Details</span> </ng-template>
Working with FormsLink to this section
Okay, so far, we have learned how we can build a custom stepper using Angular CDK and how to use it. Next, we will learn how we can use our custom stepper with Forms.
Remember to import ReactiveFormsModule
to your module:
<>Copy// other imports import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [ // ... ], imports: [ // ... ReactiveFormsModule, ], providers: [], bootstrap: [AppComponent], }) export class AppModule {}
Single FormLink to this section
In this scenario, we will have a single form across the whole stepper. To achieve this, we will use FormArrays
to create a form for each step. For instance, if we have 3 steps, we will need a form group for each step, each as part of the form array. The form array will then be a field in a larger form for the whole stepper. This will allow us to also have validator running, ensuring that we can validate different form fields if we want to. We can then submit the form at the last step of the stepper.
Component ClassLink to this section
We will start by defining our form – frmStepper
– as a component prop:
<>CopyfrmStepper: FormGroup;
Then, inside our form group, we are going to define a field called steps, which will be a FormArray
. And then, inside the FormArray
, we can create the form groups for each of our steps:
<>Copythis.frmStepper = this.fb.group({ steps: this.fb.array([ this.fb.group({ // ... form controls for our step }), // ... more form groups for each step we have ]), });
TemplateLink to this section
Inside our component template, we will have a form wrap around our stepper component. Then, we will add the formArrayName
directive, with the form field name steps
as its name:
<>Copy<form [formGroup]="frmStepper"> <app-my-stepper formArrayName="steps"> <!-- cdk steps here --> </app-my-stepper> </form>
Then, for each step, we will use the formGroupName
directive which allows us to treat each of our form groups in the array as independent form groups. We will use their indexes as the value of our formGroupName
directive. We will also add stepControl
props to our step so that we can have validation before moving to the next step:
<>Copy<cdk-step formGroupName="0" [stepControl]="formArray.get([0])"> <!-- content here --> </cdk-step>
To get the form group to use as the stepControl
props value for our step, I have created a getter for FormArray
, which returns the form group of the passed index.
<>Copy// formArray getter get formArray(): AbstractControl { return this.frmStepper.get('steps'); }
You can also enable and disable the next button, by checking if the form group for the step is valid.:
<>Copy<button [disabled]="formArray.get([1]).invalid" type="button" cdkStepperNext> Next </button>
With Multiple Forms Per StepLink to this section
The second option is to have multiple forms, instead of a single form. In this case, you will have a form group for each step.
Component ClassLink to this section
We will start by defining different forms groups for each of the steps. For instance, our demo has 3 steps – personal details, address, and payment steps. In this case, we will need three different forms.
<>CopyfrmDetails = this.fb.group({ // ... form fields here }); frmAddress = this.fb.group({ // ... form fields here }); frmPayment = this.fb.group({ // ... form fields here });
TemplateLink to this section
The form shall wrap around each steps’ content, with three different forms instead of one big one.
<>Copy<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
prop, we shall use the appropriate form group as the value. For instance, for payment form, the stepControl
value will be frmPayment
as defined in the component class:
<>Copy<cdk-step [stepControl]="frmPayment"> <!-- form here --> </cdk-step>
ConclusionLink to this section
In this article, we have learnt how to install and setup Angular CDK inside an Angular project. Then, we also learnt to build a stepper component that has the same look and feel as the rest of our application. We have also learnt how to use the stepper component in two different common scenarios – with a single form and multiple forms.
From here, we can extract the stepper component into a feature module. This is particularly helpful on large projects where Lazy Loaded implemented, as it allows the stepper feature to be available across your application while also being lazily loaded. You can take it a step further and share the stepper component across multiple projects within a workspace by using a tool like NX workspaces.
Source Code and DemoLink to this section
You can find the source code for this article here and the demo on Stack Blitz.
Extra ResourcesLink to this section
Comments (0)
Be the first to leave a comment
About the author

Software Engineer, Tech Speaker, OSS, and Mentor. Software Engineer at Skyhook Adventure, UK.

About the author
Maina Wycliffe
Software Engineer, Tech Speaker, OSS, and Mentor. Software Engineer at Skyhook Adventure, UK.
About the author

Software Engineer, Tech Speaker, OSS, and Mentor. Software Engineer at Skyhook Adventure, UK.