Angular Stepper Component – Multi-Step Form Wizard for Angular
Available in Other JavaScript Frameworks
CoreUI Angular Stepper is also available for Bootstrap, React, and Vue. Explore framework-specific implementations below:
Build multi-step forms and wizards easily with the Angular Stepper component. Create advanced form flows with custom indicators, validation, and flexible layouts for your Angular applications.
The Angular Stepper component helps you build intuitive, multi-step form experiences (Form Wizards) for your Angular applications. It supports horizontal and vertical layouts, built-in form validation, custom indicators, and seamless integration with Angular forms.
If you need a Form Wizard in Angular, or a fully customizable Angular Stepper, this component is a go to solution.
Examples
This example shows a simple multi-step form wizard built using the Angular Stepper component. Each step defines its content. Internal step navigation can be managed through provided stepper methods. Use this setup when you need a basic horizontal stepper without advanced customizations.
import { NgTemplateOutlet } from '@angular/common';
import { Component, signal } from '@angular/core';
import {
ButtonDirective,
ColComponent,
FormCheckComponent,
FormCheckInputDirective,
FormCheckLabelDirective,
FormControlDirective,
FormDirective,
FormLabelDirective,
FormPasswordDirective,
FormSelectDirective,
InputGroupComponent,
InputGroupTextDirective,
RowComponent,
StepperComponent,
StepperStepComponent
} from '@coreui/angular';
@Component({
selector: 'docs-stepper-example',
imports: [
ButtonDirective,
ColComponent,
FormCheckComponent,
FormCheckInputDirective,
FormCheckLabelDirective,
FormControlDirective,
FormDirective,
FormLabelDirective,
FormPasswordDirective,
FormSelectDirective,
InputGroupComponent,
InputGroupTextDirective,
StepperComponent,
RowComponent,
NgTemplateOutlet,
StepperStepComponent
],
templateUrl: './stepper-example.component.html',
styles: `
:host {
.btn {
margin-inline-end: 0.5rem;
}
}
`
})
export class StepperExampleComponent {
readonly finished = signal(false);
handleReset() {
console.log('- handleReset');
this.finished.set(false);
}
handleFinish(value: boolean) {
console.log('- handleFinish', value);
this.finished.set(value);
}
}<form cForm>
<c-stepper #stepper (finished)="handleFinish($event)" (onReset)="handleReset()">
<c-stepper-step label="Step 1">
<ng-container *ngTemplateOutlet="step1" />
</c-stepper-step>
<c-stepper-step label="Step 2">
<ng-container *ngTemplateOutlet="step2" />
</c-stepper-step>
<c-stepper-step label="Step 2">
<ng-container *ngTemplateOutlet="step3" />
</c-stepper-step>
</c-stepper>
</form>
<hr>
@if (stepper.finishing()) {
<button (click)="stepper.reset()" cButton color="danger">Reset</button>
} @else {
@if (stepper.activeStepIndex() > 0) {
<button cButton (click)="stepper.prev()" color="secondary">Prev</button>
}
@if (stepper.activeStepIndex() < stepper.stepsCount() - 1) {
<button cButton (click)="stepper.next()" color="primary">Next</button>
}
@if (stepper.activeStepIndex() === stepper.stepsCount() - 1) {
<button (click)="stepper.finish()" color="warning" cButton>Finish</button>
}
}
<hr>
<p>
@if (stepper.finishing()) {
All steps have been completed.
} @else {
current step: {{ stepper.activeStepIndex() + 1 }} / {{ stepper.stepsCount() }}
}
</p>
<ng-template #step1>
<c-row class="g-3 pb-5">
<c-col [md]="4">
<label cLabel for="userFirstName-01">First name</label>
<input cFormControl id="userFirstName-01">
</c-col>
<c-col [md]="4">
<label cLabel for="userLastName-01">Last name</label>
<input cFormControl id="userLastName-01">
</c-col>
<c-col [md]="4">
<label cLabel for="userName-01">Username</label>
<c-input-group>
<span cInputGroupText id="basic-addon1">@</span>
<input aria-describedby="basic-addon1"
aria-label="Username"
autocomplete="username"
cFormControl
id="userName-01"
placeholder="Username"
/>
</c-input-group>
</c-col>
</c-row>
</ng-template>
<ng-template #step2>
<c-row class="g-3 pb-5">
<c-col [md]="6">
<label cLabel for="userCity-01">City</label>
<input cFormControl id="userCity-01">
</c-col>
<c-col [md]="3">
<label cLabel for="userState-01">State</label>
<select aria-label="State select" cSelect id="userState-01">
<option>Choose...</option>
<option value="1">...</option>
</select>
</c-col>
<c-col [md]="3">
<label cLabel for="userZip-01">Zip</label>
<input cFormControl id="userZip-01">
</c-col>
</c-row>
</ng-template>
<ng-template #step3>
<c-row class="g-3 pb-1">
<c-col [md]="6">
<label cLabel for="userEmail-01">Email</label>
<input cFormControl id="userEmail-01" type="email">
</c-col>
<c-col [md]="6">
<label cLabel for="userPassword-01">Password</label>
<input autocomplete="new-password" cFormPassword id="userPassword-01">
</c-col>
<c-col>
<c-form-check>
<input
cFormCheckInput
id="userAgree-01"
required
/>
<label cFormCheckLabel for="userAgree-01">Agree to terms and conditions</label>
</c-form-check>
</c-col>
</c-row>
</ng-template>Vertical indicator
The step indicators are displayed vertically above the labels using the stepButtonLayout="vertical" prop, while the form content remains laid out horizontally. This option is useful when you want a more compact and visually balanced look for the step navigation, especially in narrower layouts. Use stepButtonLayout="vertical" when you want a clear visual separation of steps without changing the main content flow.
import { Component } from '@angular/core';
import { StepperComponent, StepperStepComponent } from '@coreui/angular';
@Component({
selector: 'docs-stepper-vertical-indicator',
imports: [StepperComponent, StepperStepComponent],
templateUrl: './stepper-vertical-indicator.component.html'
})
export class StepperVerticalIndicatorComponent {
labels = ['Step 1', 'Step 2', 'Step 3'];
}<c-stepper stepButtonLayout="vertical">
@for (label of labels; track $index) {
<c-stepper-step [label]="label" />
}
</c-stepper>Vertical layout
This example shows a fully vertical multi-step form wizard created with the Angular Stepper component. By using the layout="vertical" prop, both the step indicators and the step content are stacked vertically. This layout is ideal for mobile devices or designs where vertical flow is preferred. Choose layout="vertical" if you want the entire wizard to guide users in a top-to-bottom progression.
import { NgTemplateOutlet } from '@angular/common';
import { Component, computed, signal, TemplateRef, viewChildren } from '@angular/core';
import {
ButtonDirective,
ColComponent,
FormCheckComponent,
FormCheckInputDirective,
FormCheckLabelDirective,
FormControlDirective,
FormDirective,
FormLabelDirective,
FormPasswordDirective,
FormSelectDirective,
InputGroupComponent,
InputGroupTextDirective,
RowComponent,
StepperComponent,
StepperStepComponent
} from '@coreui/angular';
@Component({
selector: 'docs-stepper-vertical-layout',
imports: [
ButtonDirective,
ColComponent,
FormCheckComponent,
FormCheckInputDirective,
FormCheckLabelDirective,
FormControlDirective,
FormDirective,
FormLabelDirective,
FormPasswordDirective,
FormSelectDirective,
InputGroupComponent,
InputGroupTextDirective,
StepperComponent,
RowComponent,
NgTemplateOutlet,
StepperStepComponent
],
templateUrl: './stepper-vertical-layout.component.html',
styles: `
:host {
.btn {
margin-inline-end: 0.5rem;
}
}
`
})
export class StepperVerticalLayoutComponent {
readonly finished = signal(false);
handleReset() {
console.log('- handleReset');
this.finished.set(false);
}
handleFinish(value: boolean) {
console.log('- handleFinish', value);
this.finished.set(value);
}
readonly stepTemplates = viewChildren('stepTpl', { read: TemplateRef });
readonly steps = computed(() => {
const stepTemplates = this.stepTemplates();
return stepTemplates.map((step, index) => {
return { label: `Step ${index + 1}`, template: step };
});
});
}<form cForm>
<c-stepper #stepper="cStepper" (finished)="handleFinish($event)" (onReset)="handleReset()" layout="vertical">
@for (step of steps(); track $index) {
<c-stepper-step [label]="step.label">
<ng-container *ngTemplateOutlet="step.template" />
</c-stepper-step>
}
</c-stepper>
</form>
<hr>
@if (stepper.finishing()) {
<button (click)="stepper.reset()" cButton color="danger">Reset</button>
} @else {
@if (stepper.activeStepIndex() > 0) {
<button cButton (click)="stepper.prev()" color="secondary">Prev</button>
}
@if (stepper.activeStepIndex() < stepper.stepsCount() - 1) {
<button cButton (click)="stepper.next()" color="primary">Next</button>
}
@if (stepper.activeStepIndex() === stepper.stepsCount() - 1) {
<button (click)="stepper.finish()" color="warning" cButton>Finish</button>
}
}
<hr>
<p>
@if (stepper.finishing()) {
All steps have been completed.
} @else {
current step: {{ stepper.activeStepIndex() + 1 }} / {{ stepper.stepsCount() }}
}
</p>
<ng-template #step1 #stepTpl>
<c-row class="g-3 pb-5">
<c-col [md]="4">
<label cLabel for="userFirstName-03">First name</label>
<input cFormControl id="userFirstName-03">
</c-col>
<c-col [md]="4">
<label cLabel for="userLastName-03">Last name</label>
<input cFormControl id="userLastName-03">
</c-col>
<c-col [md]="4">
<label cLabel for="userName-03">Username</label>
<c-input-group>
<span cInputGroupText id="basic-addon1">@</span>
<input aria-describedby="basic-addon1"
aria-label="Username"
autocomplete="username"
cFormControl
id="userName-03"
placeholder="Username"
/>
</c-input-group>
</c-col>
</c-row>
</ng-template>
<ng-template #step2 #stepTpl>
<c-row class="g-3 pb-5">
<c-col [md]="6">
<label cLabel for="userCity-03">City</label>
<input cFormControl id="userCity-03">
</c-col>
<c-col [md]="3">
<label cLabel for="userState-03">State</label>
<select aria-label="State select" cSelect id="userState-03">
<option>Choose...</option>
<option value="1">...</option>
</select>
</c-col>
<c-col [md]="3">
<label cLabel for="userZip-03">Zip</label>
<input cFormControl id="userZip-03">
</c-col>
</c-row>
</ng-template>
<ng-template #step3 #stepTpl>
<c-row class="g-3 pb-1">
<c-col [md]="6">
<label cLabel for="userEmail-03">Email</label>
<input cFormControl id="userEmail-03" type="email">
</c-col>
<c-col [md]="6">
<label cLabel for="userPassword-03">Password</label>
<input autocomplete="new-password" cFormPassword id="userPassword-03">
</c-col>
<c-col>
<c-form-check>
<input
cFormCheckInput
id="userAgree-03"
required
/>
<label cFormCheckLabel for="userAgree-03">Agree to terms and conditions</label>
</c-form-check>
</c-col>
</c-row>
</ng-template>Linear Form Wizard
By default, the Angular Component behaves as a linear wizard: users must complete each step sequentially before moving to the next one.
Linear mode is enabled by default [linear]="true", users cannot skip steps. They must finish the current step to unlock the next.
Use a Linear Angular Stepper when you need a guided and controlled experience, such as:
- Checkout process
- Registration wizard
- Multistep forms with validation
This ensures data integrity and improves the user experience by keeping the flow focused.
import { Component } from '@angular/core';
import { StepperComponent, StepperStepComponent } from '@coreui/angular';
@Component({
selector: 'docs-stepper-linear-form-wizard',
imports: [StepperComponent, StepperStepComponent],
templateUrl: './stepper-linear-form-wizard.component.html'
})
export class StepperLinearFormWizardComponent {}<c-stepper>
<c-stepper-step label="Step 1">One</c-stepper-step>
<c-stepper-step label="Step 2">Two</c-stepper-step>
<c-stepper-step label="Step 3">Three</c-stepper-step>
</c-stepper>Non-linear Stepper
You can configure the Angular Stepper Component to behave as non-linear, allowing users to jump freely between steps without validation restrictions. Set [linear]="false" property to allow non-sequential navigation.
Use a Non-linear Angular Stepper when users should have full control over navigation, for example:
- Survey forms
- Onboarding flows where some steps are optional
- Complex multi-section forms where order doesn’t matter
In non-linear mode, all steps are accessible unless explicitly disabled.
import { Component } from '@angular/core';
import { StepperComponent, StepperStepComponent } from '@coreui/angular';
@Component({
selector: 'docs-stepper-non-linear',
imports: [StepperComponent, StepperStepComponent],
templateUrl: './stepper-non-linear.component.html'
})
export class StepperNonLinearComponent {}<c-stepper #stepper [linear]="false">
<c-stepper-step label="Step 1" />
<c-stepper-step label="Step 2" />
<c-stepper-step disabled label="(disabled)" />
<c-stepper-step label="Step 4" />
</c-stepper>Form Validation
The Angular Stepper component natively supports step-by-step form validation. Use this feature to ensure required fields are filled and basic data integrity is maintained throughout the multi-step process without needing any additional code.
The following example demonstrates how to use the Angular Stepper with HTML5 validation and template-driven form. Each step contains a form, and when the validation prop is enabled, users must complete each form before proceeding to the next step. If a form is invalid, the stepper prevents navigation until the form is valid.
Browser Validation
import { JsonPipe, NgTemplateOutlet } from '@angular/common';
import { Component, signal, viewChild, viewChildren } from '@angular/core';
import {
BadgeComponent,
ButtonDirective,
ColComponent,
FormCheckComponent,
FormCheckInputDirective,
FormCheckLabelDirective,
FormControlDirective,
FormDirective,
FormLabelDirective,
FormPasswordDirective,
FormSelectDirective,
InputGroupComponent,
InputGroupTextDirective,
RowComponent,
StepperComponent,
StepperStepComponent
} from '@coreui/angular';
import { FormsModule, NgForm, NgModelGroup, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'docs-stepper-browser-validation',
imports: [
ButtonDirective,
ColComponent,
FormCheckComponent,
FormCheckInputDirective,
FormCheckLabelDirective,
FormControlDirective,
FormLabelDirective,
FormPasswordDirective,
FormSelectDirective,
InputGroupComponent,
InputGroupTextDirective,
StepperComponent,
RowComponent,
NgTemplateOutlet,
ReactiveFormsModule,
FormsModule,
FormDirective,
JsonPipe,
BadgeComponent,
StepperStepComponent
],
templateUrl: './stepper-browser-validation.component.html',
styles: `
:host {
.btn {
margin-inline-end: 0.5rem;
}
}
`
})
export class StepperBrowserValidationComponent {
readonly stepGroups: Record<string, any>[] = [
{
firstName: 'Lukasz',
lastName: 'Holeczek',
userName: null
},
{
address: 'Anchorage',
state: undefined,
zip: '99599'
},
{
email: 'john@doe',
password: null,
terms: false
}
];
readonly stepper = viewChild.required<StepperComponent>(StepperComponent);
readonly stepperForms = viewChildren(NgForm);
readonly stepperFormGroups = viewChildren(NgModelGroup);
readonly finished = signal(false);
handleReset() {
this.stepperForms().forEach((form) => {
form.reset();
});
this.finished.set(false);
}
handleFinish(finish: boolean) {
if (!finish) {
return false;
}
this.finished.set(finish);
return true;
}
onSubmit() {
this.stepper().next();
return true;
}
readonly states = [
{ name: 'Alabama', code: 'AL' },
{ name: 'Alaska', code: 'AK' }
];
}@let forms = stepperForms();
@let frmGroups = stepperFormGroups();
<c-stepper #stepper (finished)="handleFinish($event)" (onReset)="handleReset()" validation>
<c-stepper-step [valid]="$safeNavigationMigration(frmGroups[0]?.valid)" label="Step 1">
<ng-container *ngTemplateOutlet="tplStep0" />
</c-stepper-step>
<c-stepper-step [valid]="$safeNavigationMigration(frmGroups[1]?.valid)" label="Step 2">
<ng-container *ngTemplateOutlet="tplStep1" />
</c-stepper-step>
<c-stepper-step [valid]="$safeNavigationMigration(frmGroups[2]?.valid)" label="Step 3">
<ng-container *ngTemplateOutlet="tplStep2" />
</c-stepper-step>
</c-stepper>
<p>
@if (stepper.finishing()) {
<p>All steps have been completed.</p>
@for (form of forms; track $index) {
<div class="mb-0">
<code>{{ form.value | json }}</code>
</div>
}
@let disableReset = !stepper.finishing();
<hr />
<button (click)="stepper.reset()" [disabled]="disableReset" cButton color="danger" type="reset">Reset</button>
}
</p>
<ng-template #navButtons>
<hr />
@let disablePrev = stepper.activeStepIndex() === 0 || stepper.finishing();
<button (click)="stepper.prev()" [disabled]="disablePrev" cButton color="info">Prev</button>
@let finished = stepper.finishing();
@let nextCaption = stepper.activeStepIndex() === stepper.stepsCount() - 1 && !finished ? 'Finish' : 'Next';
<button [disabled]="finished" cButton color="primary" type="submit">{{ nextCaption }}</button>
@let stepValid = stepperForms()[stepper.activeStepIndex()].valid;
current step: {{ stepper.activeStepIndex() + 1 }} / {{ stepper.stepsCount() }}
<c-badge [color]="stepValid ? 'success' : 'danger'">{{ stepperForms()[stepper.activeStepIndex()].status }}</c-badge
><br />
</ng-template>
<ng-template #tplStep0>
<form (ngSubmit)="onSubmit()" autocomplete="off" cForm ngForm ngNativeValidate>
@let stepGroup = stepGroups[0];
<c-row class="g-3 pb-5" ngModelGroup="step_0">
<c-col [md]="4">
<label cLabel for="userFirstName-06">First name</label>
<input [ngModel]="stepGroup['firstName']" cFormControl id="userFirstName-06" name="firstName" required />
</c-col>
<c-col [md]="4">
<label cLabel for="userLastName-06">Last name</label>
<input [ngModel]="stepGroup['lastName']" cFormControl id="userLastName-06" name="lastName" required />
</c-col>
<c-col [md]="4">
<label cLabel for="userName-06">Username</label>
<c-input-group>
<span cInputGroupText id="basic-addon1">@</span>
<input
[ngModel]="stepGroup['userName']"
aria-describedby="basic-addon1"
aria-label="Username"
autocomplete="off"
cFormControl
id="userName-06"
minlength="5"
name="userName"
required
/>
</c-input-group>
</c-col>
</c-row>
<ng-container *ngTemplateOutlet="navButtons" />
</form>
</ng-template>
<ng-template #tplStep1>
<form (ngSubmit)="onSubmit()" autocomplete="off" cForm ngForm ngNativeValidate>
@let stepGroup = stepGroups[1];
<c-row class="g-3 pb-5" ngModelGroup="step_1">
<c-col [md]="6">
<label cLabel for="userAddress-06">City</label>
<input
[ngModel]="stepGroup['address']"
cFormControl
id="userAddress-06"
name="address"
required
autocomplete="off"
/>
</c-col>
<c-col [md]="3">
<label cLabel for="userState-06">State</label>
<select
[ngModel]="stepGroup['state']"
aria-label="State select"
cSelect
id="userState-06"
name="state"
required
>
<option disabled>Choose...</option>
@for (state of states; track state.code) {
<option [ngValue]="state.code">{{ state.name }}</option>
}
</select>
</c-col>
<c-col [md]="3">
<label cLabel for="userZip-06">Zip</label>
<input [ngModel]="stepGroup['zip']" cFormControl id="userZip-06" name="zip" required />
</c-col>
</c-row>
<ng-container *ngTemplateOutlet="navButtons" />
</form>
</ng-template>
<ng-template #tplStep2>
<form (ngSubmit)="onSubmit()" autocomplete="off" cForm ngForm ngNativeValidate>
@let stepGroup = stepGroups[2];
<c-row class="g-3 pb-1" ngModelGroup="step_2">
<c-col [md]="6">
<label cLabel for="userEmail-06">Email</label>
<input [ngModel]="stepGroup['email']" cFormControl id="userEmail-06" name="email" required type="email" />
</c-col>
<c-col [md]="6">
<label cLabel for="userPassword-06">Password</label>
<input
[ngModel]="stepGroup['password']"
autocomplete="new-password"
cFormPassword
id="userPassword-06"
minlength="5"
name="password"
required
/>
</c-col>
<c-col>
<c-form-check>
<input
[ngModel]="stepGroup['terms']"
cFormCheckInput
id="userTerms-06"
name="terms"
required
type="checkbox"
/>
<label cFormCheckLabel for="userTerms-06">Agree to terms and conditions</label>
</c-form-check>
</c-col>
</c-row>
<ng-container *ngTemplateOutlet="navButtons" />
</form>
</ng-template>Custom Validation
Beyond default HTML5 validation, you can define custom validation rules for each step. This allows you to implement complex form validation logic per step. In the example below using Angular reactive forms, custom validation triggers additional UI feedback.
import { JsonPipe, NgTemplateOutlet } from '@angular/common';
import { Component, signal } from '@angular/core';
import {
BadgeComponent,
ButtonDirective,
ColComponent,
FormCheckComponent,
FormCheckInputDirective,
FormCheckLabelDirective,
FormControlDirective,
FormLabelDirective,
FormPasswordDirective,
FormSelectDirective,
InputGroupComponent,
InputGroupTextDirective,
RowComponent,
StepperComponent,
StepperStepComponent
} from '@coreui/angular';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
selector: 'docs-stepper-custom-validation',
imports: [
BadgeComponent,
ButtonDirective,
ColComponent,
FormCheckComponent,
FormCheckInputDirective,
FormCheckLabelDirective,
FormControlDirective,
FormLabelDirective,
FormPasswordDirective,
FormSelectDirective,
InputGroupComponent,
InputGroupTextDirective,
StepperComponent,
RowComponent,
NgTemplateOutlet,
ReactiveFormsModule,
FormsModule,
JsonPipe,
StepperStepComponent
],
templateUrl: './stepper-custom-validation.component.html',
styles: `
:host {
.btn {
margin-inline-end: 0.5rem;
}
}
`
})
export class StepperCustomValidationComponent {
readonly stepperForm: FormGroup = new FormGroup({
step_0: new FormGroup({
firstName: new FormControl('Lukasz', { validators: [Validators.required] }),
lastName: new FormControl('Holeczek', { validators: [Validators.required] }),
userName: new FormControl('', { validators: [Validators.required, Validators.minLength(5)] })
}),
step_1: new FormGroup({
address: new FormControl('Anchorage', { validators: [Validators.required] }),
state: new FormControl('AK', { validators: [Validators.required] }),
zip: new FormControl('99599', { validators: [Validators.required] })
}),
step_2: new FormGroup({
email: new FormControl('john@doe', { validators: [Validators.required, Validators.email] }),
password: new FormControl('', { validators: [Validators.required, Validators.minLength(5)] }),
terms: new FormControl(false, { validators: [Validators.requiredTrue] })
})
});
readonly formGroups = Object.values(this.stepperForm.controls);
readonly group_0 = this.stepperForm.get('step_0') as FormGroup;
readonly group_1 = this.stepperForm.get('step_1') as FormGroup;
readonly group_2 = this.stepperForm.get('step_2') as FormGroup;
readonly finished = signal(false);
readonly currentStep = signal(0);
handleReset() {
this.stepperForm.reset();
this.finished.set(false);
}
handleFinish(finish: boolean) {
if (!finish) {
return false;
}
const valid = this.currentFormGroupValid(this.currentStep());
if (!valid) {
// return false;
}
this.finished.set(finish);
return true;
}
handleNext(stepper: StepperComponent) {
const valid = this.currentFormGroupValid(this.currentStep());
if (!valid) {
// return false;
}
stepper.next();
}
currentFormGroupValid(step: number) {
const currentGroup = `group_${step}` as keyof StepperCustomValidationComponent;
const currentFormGroup = this[currentGroup] as FormGroup;
currentFormGroup.markAllAsTouched();
return currentFormGroup?.valid;
}
readonly states = [
{ name: 'Alabama', code: 'AL' },
{ name: 'Alaska', code: 'AK' }
];
}<form [formGroup]="stepperForm">
<c-stepper
#stepper
(finished)="handleFinish($event)"
(onReset)="handleReset()"
[(activeStepIndex)]="currentStep"
validation
>
<c-stepper-step [valid]="group_0.valid" label="Step 1">
<ng-container *ngTemplateOutlet="step0" />
</c-stepper-step>
<c-stepper-step [valid]="group_1.valid" label="Step 2">
<ng-container *ngTemplateOutlet="step1" />
</c-stepper-step>
<c-stepper-step [valid]="group_2.valid" label="Step 2">
<ng-container *ngTemplateOutlet="step2" />
</c-stepper-step>
</c-stepper>
</form>
<span>
@if (stepper.finishing()) {
<p>All steps have been completed.</p>
@for (form of formGroups; track $index) {
<div class="mb-0">
<code>{{ form.value | json }}</code>
</div>
}
@let disableReset = !stepper.finishing();
<hr />
<button (click)="stepper.reset()" [disabled]="disableReset" cButton color="danger">Reset</button>
} @else {
<ng-container *ngTemplateOutlet="navButtons" />
}
</span>
<ng-template #navButtons>
<hr />
@let disablePrev = stepper.activeStepIndex() === 0 || stepper.finishing();
<button (click)="stepper.prev()" [disabled]="disablePrev" cButton color="info">Prev</button>
@let finished = stepper.finishing();
@let nextCaption = stepper.activeStepIndex() === stepper.stepsCount() - 1 && !finished ? 'Finish' : 'Next';
<button (click)="handleNext(stepper)" [disabled]="finished" cButton color="primary">{{ nextCaption }}</button>
@let step = formGroups[stepper.activeStepIndex()];
current step: {{ stepper.activeStepIndex() + 1 }} / {{ stepper.stepsCount() }}
<c-badge [color]="step.valid ? 'success' : 'danger'">{{ step.status }}</c-badge>
</ng-template>
<ng-template #step0>
@let group = group_0;
@let firstName = group.get('firstName');
@let lastName = group.get('lastName');
@let userName = group.get('userName');
<!-- @let ctrl = group.controls;-->
<c-row [formGroup]="group" class="g-3 pb-5">
<c-col [md]="4">
<label cLabel for="userFirstName-07">First name</label>
<input
[valid]="firstName?.touched ? $safeNavigationMigration(firstName?.valid) : undefined"
cFormControl
formControlName="firstName"
id="userFirstName-07"
/>
</c-col>
<c-col [md]="4">
<label cLabel for="userLastName-07">Last name</label>
<input
[valid]="lastName?.touched ? $safeNavigationMigration(lastName?.valid) : undefined"
cFormControl
formControlName="lastName"
id="userLastName-07"
/>
</c-col>
<c-col [md]="4">
<label cLabel for="userName-07">Username</label>
<c-input-group>
<span cInputGroupText id="basic-addon1">@</span>
<input
[valid]="userName?.touched ? $safeNavigationMigration(userName?.valid) : undefined"
aria-describedby="basic-addon1"
aria-label="Username"
autocomplete="off"
cFormControl
formControlName="userName"
id="userName-07"
required
/>
</c-input-group>
</c-col>
</c-row>
</ng-template>
<ng-template #step1>
@let group = group_1;
@let address = group.get('address');
@let state = group.get('state');
@let zip = group.get('zip');
<c-row [formGroup]="group" class="g-3 pb-5">
<c-col [md]="6">
<label cLabel for="userAddress-07">City</label>
<input
[valid]="address?.touched ? $safeNavigationMigration(address?.valid) : undefined"
cFormControl
formControlName="address"
id="userAddress-07"
/>
</c-col>
<c-col [md]="3">
<label cLabel for="userState-07">State</label>
<select
[valid]="state?.touched ? $safeNavigationMigration(state?.valid) : undefined"
aria-label="State select"
cSelect
formControlName="state"
id="userState-07"
>
<option disabled>Choose...</option>
@for (state of states; track state.code) {
<option [ngValue]="state.code">{{ state.name }}</option>
}
</select>
</c-col>
<c-col [md]="3">
<label cLabel for="userZip-07">Zip</label>
<input
[valid]="zip?.touched ? $safeNavigationMigration(zip?.valid) : undefined"
cFormControl
formControlName="zip"
id="userZip-07"
/>
</c-col>
</c-row>
</ng-template>
<ng-template #step2>
@let group = group_2;
@let email = group.get('email');
@let password = group.get('password');
@let terms = group.get('terms');
<c-row [formGroup]="group" class="g-3 pb-1">
<c-col [md]="6">
<label cLabel for="userEmail-07">Email</label>
<input
[valid]="email?.touched ? $safeNavigationMigration(email?.valid) : undefined"
cFormControl
formControlName="email"
id="userEmail-07"
type="email"
/>
</c-col>
<c-col [md]="6">
<label cLabel for="userPassword-07">Password</label>
<input
[valid]="password?.touched ? $safeNavigationMigration(password?.valid) : undefined"
autocomplete="new-password"
cFormPassword
formControlName="password"
id="userPassword-07"
/>
</c-col>
<c-col>
<c-form-check>
<input
[valid]="terms?.touched ? $safeNavigationMigration(terms?.valid) : undefined"
cFormCheckInput
formControlName="terms"
id="userTerms-07"
type="checkbox"
/>
<label cFormCheckLabel for="userTerms-07">Agree to terms and conditions</label>
</c-form-check>
</c-col>
</c-row>
</ng-template>Skip validation
To completely skip form validation and allow free navigation between steps, add [validation]="false" to the Angular Stepper component:
import { JsonPipe, NgTemplateOutlet } from '@angular/common';
import { Component, signal } from '@angular/core';
import {
BadgeComponent,
ButtonDirective,
ColComponent,
FormCheckComponent,
FormCheckInputDirective,
FormCheckLabelDirective,
FormControlDirective,
FormLabelDirective,
FormPasswordDirective,
FormSelectDirective,
InputGroupComponent,
InputGroupTextDirective,
RowComponent,
StepperComponent,
StepperStepComponent
} from '@coreui/angular';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
selector: 'docs-stepper-skip-validation',
imports: [
BadgeComponent,
ButtonDirective,
ColComponent,
FormCheckComponent,
FormCheckInputDirective,
FormCheckLabelDirective,
FormControlDirective,
FormLabelDirective,
FormPasswordDirective,
FormSelectDirective,
InputGroupComponent,
InputGroupTextDirective,
StepperComponent,
RowComponent,
NgTemplateOutlet,
ReactiveFormsModule,
FormsModule,
JsonPipe,
StepperStepComponent
],
templateUrl: './stepper-skip-validation.component.html',
styles: `
:host {
.btn {
margin-inline-end: 0.5rem;
}
}
`
})
export class StepperSkipValidationComponent {
readonly stepperForm: FormGroup = new FormGroup({
step_0: new FormGroup({
firstName: new FormControl('Lukasz', { validators: [Validators.required] }),
lastName: new FormControl('Holeczek', { validators: [Validators.required] }),
userName: new FormControl('', { validators: [Validators.required, Validators.minLength(5)] })
}),
step_1: new FormGroup({
address: new FormControl('Anchorage', { validators: [Validators.required] }),
state: new FormControl('AK', { validators: [Validators.required] }),
zip: new FormControl('99599', { validators: [Validators.required] })
}),
step_2: new FormGroup({
email: new FormControl('john@doe', { validators: [Validators.required, Validators.email] }),
password: new FormControl('', { validators: [Validators.required, Validators.minLength(5)] }),
terms: new FormControl(false, { validators: [Validators.requiredTrue] })
})
});
readonly formGroups = Object.values(this.stepperForm.controls);
readonly group_0 = this.stepperForm.get('step_0') as FormGroup;
readonly group_1 = this.stepperForm.get('step_1') as FormGroup;
readonly group_2 = this.stepperForm.get('step_2') as FormGroup;
readonly finished = signal(false);
readonly currentStep = signal(0);
handleReset() {
this.stepperForm.reset();
this.finished.set(false);
}
handleFinish(finish: boolean) {
if (!finish) {
return false;
}
const valid = this.currentFormGroupValid(this.currentStep());
if (!valid) {
// return false;
}
this.finished.set(finish);
return true;
}
handleNext(stepper: StepperComponent) {
const valid = this.currentFormGroupValid(this.currentStep());
if (!valid) {
// return false;
}
stepper.next();
}
currentFormGroupValid(step: number) {
const currentGroup = `group_${step}` as keyof StepperSkipValidationComponent;
const currentFormGroup = this[currentGroup] as FormGroup;
currentFormGroup.markAllAsTouched();
return currentFormGroup?.valid;
}
readonly states = [
{ name: 'Alabama', code: 'AL' },
{ name: 'Alaska', code: 'AK' }
];
}<form [formGroup]="stepperForm">
<c-stepper
#stepper
(finished)="handleFinish($event)"
(onReset)="handleReset()"
[(activeStepIndex)]="currentStep"
[validation]="false"
[linear]="false"
>
<c-stepper-step [valid]="group_0.valid" label="Step 1">
<ng-container *ngTemplateOutlet="step0" />
</c-stepper-step>
<c-stepper-step [valid]="group_1.valid" label="Step 2">
<ng-container *ngTemplateOutlet="step1" />
</c-stepper-step>
<c-stepper-step [valid]="group_2.valid" label="Step 2">
<ng-container *ngTemplateOutlet="step2" />
</c-stepper-step>
</c-stepper>
</form>
<span>
@if (stepper.finishing()) {
<p>All steps have been completed.</p>
@for (form of formGroups; track $index) {
<div class="mb-0">
<code>{{ form.value | json }}</code>
</div>
}
@let disableReset = !stepper.finishing();
<hr />
<button (click)="stepper.reset()" [disabled]="disableReset" cButton color="danger">Reset</button>
} @else {
<ng-container *ngTemplateOutlet="navButtons" />
}
</span>
<ng-template #navButtons>
<hr />
@let disablePrev = stepper.activeStepIndex() === 0 || stepper.finishing();
<button (click)="stepper.prev()" [disabled]="disablePrev" cButton color="info">Prev</button>
@let finished = stepper.finishing();
@let nextCaption = stepper.activeStepIndex() === stepper.stepsCount() - 1 && !finished ? 'Finish' : 'Next';
<button (click)="handleNext(stepper)" [disabled]="finished" cButton color="primary">{{ nextCaption }}</button>
@let step = formGroups[stepper.activeStepIndex()];
current step: {{ stepper.activeStepIndex() + 1 }} / {{ stepper.stepsCount() }}
<c-badge [color]="step.valid ? 'success' : 'danger'">{{ step.status }}</c-badge>
</ng-template>
<ng-template #step0>
@let group = group_0;
@let firstName = group.get('firstName');
@let lastName = group.get('lastName');
@let userName = group.get('userName');
<!-- @let ctrl = group.controls;-->
<c-row [formGroup]="group" class="g-3 pb-5">
<c-col [md]="4">
<label cLabel for="userFirstName-08">First name</label>
<input
[valid]="firstName?.touched ? $safeNavigationMigration(firstName?.valid) : undefined"
cFormControl
formControlName="firstName"
id="userFirstName-08"
/>
</c-col>
<c-col [md]="4">
<label cLabel for="userLastName-08">Last name</label>
<input
[valid]="lastName?.touched ? $safeNavigationMigration(lastName?.valid) : undefined"
cFormControl
formControlName="lastName"
id="userLastName-08"
/>
</c-col>
<c-col [md]="4">
<label cLabel for="userName-08">Username</label>
<c-input-group>
<span cInputGroupText id="basic-addon1">@</span>
<input
[valid]="userName?.touched ? $safeNavigationMigration(userName?.valid) : undefined"
aria-describedby="basic-addon1"
aria-label="Username"
autocomplete="off"
cFormControl
formControlName="userName"
id="userName-08"
required
/>
</c-input-group>
</c-col>
</c-row>
</ng-template>
<ng-template #step1>
@let group = group_1;
@let address = group.get('address');
@let state = group.get('state');
@let zip = group.get('zip');
<c-row [formGroup]="group" class="g-3 pb-5">
<c-col [md]="6">
<label cLabel for="userAddress-08">City</label>
<input
[valid]="address?.touched ? $safeNavigationMigration(address?.valid) : undefined"
cFormControl
formControlName="address"
id="userAddress-08"
/>
</c-col>
<c-col [md]="3">
<label cLabel for="userState-08">State</label>
<select
[valid]="state?.touched ? $safeNavigationMigration(state?.valid) : undefined"
aria-label="State select"
cSelect
formControlName="state"
id="userState-08"
>
<option disabled>Choose...</option>
@for (state of states; track state.code) {
<option [ngValue]="state.code">{{ state.name }}</option>
}
</select>
</c-col>
<c-col [md]="3">
<label cLabel for="userZip-08">Zip</label>
<input
[valid]="zip?.touched ? $safeNavigationMigration(zip?.valid) : undefined"
cFormControl
formControlName="zip"
id="userZip-08"
/>
</c-col>
</c-row>
</ng-template>
<ng-template #step2>
@let group = group_2;
@let email = group.get('email');
@let password = group.get('password');
@let terms = group.get('terms');
<c-row [formGroup]="group" class="g-3 pb-1">
<c-col [md]="6">
<label cLabel for="userEmail-08">Email</label>
<input
[valid]="email?.touched ? $safeNavigationMigration(email?.valid) : undefined"
cFormControl
formControlName="email"
id="userEmail-08"
type="email"
/>
</c-col>
<c-col [md]="6">
<label cLabel for="userPassword-08">Password</label>
<input
[valid]="password?.touched ? $safeNavigationMigration(password?.valid) : undefined"
autocomplete="new-password"
cFormPassword
formControlName="password"
id="userPassword-08"
/>
</c-col>
<c-col>
<c-form-check>
<input
[valid]="terms?.touched ? $safeNavigationMigration(terms?.valid) : undefined"
cFormCheckInput
formControlName="terms"
id="userTerms-08"
type="checkbox"
/>
<label cFormCheckLabel for="userTerms-08">Agree to terms and conditions</label>
</c-form-check>
</c-col>
</c-row>
</ng-template>Accessibility (a11y)
The CoreUI Stepper Component is built with accessibility in mind:
- Each step button is assigned proper ARIA roles (
role="tab") and attributes likearia-selected,aria-controls, andtabindex. - Step contents (stepper-pane) use
role="tabpanel"and are properly linked to their trigger buttons. - Live updates are announced to screen readers with
aria-live="polite". - Keyboard navigation is fully supported. Thanks to these features, your form wizard will be fully compliant with WCAG and modern accessibility standards without additional work.
Keyboard Support
The Stepper component supports comprehensive keyboard navigation out of the box:
| Key | Function | Note |
|---|---|---|
| ArrowLeft | Moves focus to previous step | layout=“horizontal” |
| ArrowRight | Moves focus to next step | |
| ArrowUp | Moves focus to previous step | layout=“vertical” |
| ArrowDown | Moves focus to next step | |
| Home | Moves focus to the first step | [linear]=“false” |
| End | Moves focus to the last step |
Customizing
CSS variables
Angular CoreUI Stepper use local CSS variables for enhanced real-time customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too.
.stepper {
--cui-stepper-steps-gap: #{$stepper-steps-gap};
--cui-stepper-step-gap: #{$stepper-step-gap};
--cui-stepper-step-button-width: #{$stepper-step-button-width};
--cui-stepper-step-button-color: #{$stepper-step-button-color};
--cui-stepper-step-button-active-color: #{$stepper-step-button-active-color};
--cui-stepper-step-button-complete-color: #{$stepper-step-button-complete-color};
--cui-stepper-step-button-disabled-color: #{$stepper-step-button-disabled-color};
--cui-stepper-step-indicator-width: #{$stepper-step-indicator-width};
--cui-stepper-step-indicator-height: #{$stepper-step-indicator-height};
--cui-stepper-step-indicator-bg: #{$stepper-step-indicator-bg};
--cui-stepper-step-indicator-color: #{$stepper-step-indicator-color};
--cui-stepper-step-indicator-border-width: #{$stepper-step-indicator-border-width};
--cui-stepper-step-indicator-border-color: #{$stepper-step-indicator-border-color};
--cui-stepper-step-indicator-transition: #{$stepper-step-indicator-transition};
--cui-stepper-step-indicator-active-color: #{$stepper-step-indicator-active-color};
--cui-stepper-step-indicator-active-bg: #{$stepper-step-indicator-active-bg};
--cui-stepper-step-indicator-active-border-color: #{$stepper-step-indicator-active-border-color};
--cui-stepper-step-indicator-complete-color: #{$stepper-step-indicator-complete-color};
--cui-stepper-step-indicator-complete-bg: #{$stepper-step-indicator-complete-bg};
--cui-stepper-step-indicator-complete-border-color: #{$stepper-step-indicator-complete-border-color};
--cui-stepper-step-indicator-disabled-color: #{$stepper-step-indicator-disabled-color};
--cui-stepper-step-indicator-disabled-bg: #{$stepper-step-indicator-disabled-bg};
--cui-stepper-step-indicator-disabled-border-color: #{$stepper-step-indicator-disabled-border-color};
--cui-stepper-step-indicator-focus-box-shadow: #{$stepper-step-indicator-focus-box-shadow};
--cui-stepper-step-indicator-icon: #{$stepper-step-indicator-icon};
--cui-stepper-step-indicator-icon-color: #{$stepper-step-indicator-icon-color};
--cui-stepper-step-indicator-icon-size: #{$stepper-step-indicator-icon-size};
--cui-stepper-step-connector-height: #{$stepper-step-connector-height};
--cui-stepper-step-connector-gap: #{$stepper-step-connector-gap};
--cui-stepper-step-connector-bg: #{$stepper-step-connector-bg};
--cui-stepper-step-connector-complete-bg: #{$stepper-step-connector-complete-bg};
--cui-stepper-step-connector-transition: #{$stepper-step-connector-transition};
--cui-stepper-step-content-transition: #{$stepper-step-content-transition};
}How to use CSS variables
import { Component } from '@angular/core';
import { NgStyle } from '@angular/common';
import { StepperComponent, StepperStepComponent } from '@coreui/angular';
@Component({
selector: 'docs-stepper-css-variables',
imports: [NgStyle, StepperComponent, StepperStepComponent],
templateUrl: './stepper-css-variables.component.html'
})
export class StepperCssVariablesComponent {}@let vars = {
'--cui-stepper-step-button-complete-color': 'var(--cui-success)',
'--cui-stepper-step-indicator-complete-color': 'var(--cui-success)',
'--cui-stepper-step-indicator-complete-bg': 'var(--cui-success)',
'--cui-stepper-step-indicator-complete-border-color': 'var(--cui-success)',
'--cui-stepper-step-connector-complete-bg': 'var(--cui-success)',
};
<c-stepper [activeStepIndex]="1" [linear]="false" [ngStyle]="vars">
<c-stepper-step label="Step 1"></c-stepper-step>
<c-stepper-step label="Step 2"></c-stepper-step>
<c-stepper-step label="Step 3"></c-stepper-step>
</c-stepper>SASS variables
$stepper-steps-gap: .5rem !default;
$stepper-step-gap: .5rem !default;
$stepper-step-button-width: 8rem !default;
$stepper-step-button-color: var(--cui-secondary-color) !default;
$stepper-step-button-active-color: var(--cui-secondary-color) !default;
$stepper-step-button-complete-color: var(--cui-secondary-color) !default;
$stepper-step-button-disabled-color: var(--cui-secondary-color) !default;
$stepper-step-indicator-width: 2rem !default;
$stepper-step-indicator-height: 2rem !default;
$stepper-step-indicator-color: var(--cui-secondary) !default;
$stepper-step-indicator-bg: transparent !default;
$stepper-step-indicator-border-width: var(--cui-border-width) !default;
$stepper-step-indicator-border-color: var(--cui-border-color) !default;
$stepper-step-indicator-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;
$stepper-step-indicator-active-color: var(--cui-primary) !default;
$stepper-step-indicator-active-bg: rgba(var(--cui-primary-rgb), .05) !default;
$stepper-step-indicator-active-border-color: var(--cui-primary) !default;
$stepper-step-indicator-complete-color: var(--cui-white) !default;
$stepper-step-indicator-complete-bg: var(--cui-primary) !default;
$stepper-step-indicator-complete-border-color: var(--cui-primary) !default;
$stepper-step-indicator-disabled-color: var(--cui-secondary) !default;
$stepper-step-indicator-disabled-bg: transparent !default;
$stepper-step-indicator-disabled-border-color: var(--cui-border-color) !default;
$stepper-step-indicator-focus-box-shadow: $focus-ring-box-shadow !default;
$stepper-step-indicator-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpolygon fill='var(--ci-primary-color, currentColor)' points='200.359 382.269 61.057 251.673 82.943 228.327 199.641 337.731 428.686 108.687 451.314 131.313 200.359 382.269' class='ci-primary'/%3E%3C/svg%3E") !default;
$stepper-step-indicator-icon-color: var(--cui-white) !default;
$stepper-step-indicator-icon-size: 1rem !default;
$stepper-step-connector-height: .125rem !default;
$stepper-step-connector-gap: 1rem !default;
$stepper-step-connector-bg: var(--cui-secondary-bg) !default;
$stepper-step-connector-complete-bg: var(--cui-primary) !default;
$stepper-step-connector-transition: background-color .15s ease-in-out !default;
$stepper-step-content-transition: height .3s ease-in-out !default;Button Templates
The Angular Stepper component allows you to customize step buttons using templates. This enables you to create unique step indicators, labels, and even add icons or custom HTML content.
import { Component, inject } from '@angular/core';
import { StepperComponent, StepperStepComponent } from '@coreui/angular';
import { IconDirective, IconSetService } from '@coreui/icons-angular';
import { cilExitToApp, cilPen, cilSend, cilUser } from '@coreui/icons';
@Component({
selector: 'docs-stepper-button-templates',
imports: [StepperComponent, IconDirective, StepperStepComponent],
templateUrl: './stepper-button-templates.component.html',
providers: [IconSetService]
})
export class StepperButtonTemplatesComponent {
readonly iconSet = inject(IconSetService);
constructor() {
this.iconSet.icons = {
cilExitToApp,
cilSend,
cilPen,
cilUser
};
}
}<c-stepper>
<c-stepper-step [indicatorCtx]="{ icon: 'cilUser' }" [indicator]="indicatorTpl" label="User" />
<c-stepper-step [label]="labelTpl2" />
<c-stepper-step
[indicatorCtx]="{ icon: 'cilSend' }"
[indicator]="indicatorTpl"
[labelCtx]="{ icon: 'cilExitToApp', caption: 'Send' }"
[label]="labelTpl"
/>
</c-stepper>
<ng-template #indicatorTpl let-icon="icon">
<svg [name]="icon" cIcon title="Icon"></svg>
</ng-template>
<ng-template #labelTpl let-caption="caption" let-icon="icon">
<span class="text-danger">
{{ caption }} <svg [name]="icon" cIcon title="Icon"></svg>
</span>
</ng-template>
<ng-template #labelTpl2>
<span> Sign <svg cIcon name="cilPen" title="Icon"></svg></span>
</ng-template>API reference
Stepper Module
import { NgModule } from '@angular/core';
import { StepperModule } from '@coreui/angular';
@NgModule({
imports: [StepperModule]
})
export class CustomAppModule {}Stepper Standalone
import { Component } from '@angular/core';
import { StepContentComponent, StepperComponent } from '@coreui/angular';
@Component({
template: `
<c-stepper>
<c-stepper-step label="Step 1">
<p>This is the content of step 1.</p>
</c-stepper-step>
</c-stepper>
`,
imports: [StepperComponent, StepContentComponent],
standalone: true
})
export class CustomAppComponent {}c-stepper
component
import { StepperComponent } from '@coreui/angular-pro'Props
Events
Methods
| name | description | type |
|---|---|---|
finish | Triggers the finish action, marking the stepper as completed. | void |
reset | Resets the stepper to its initial state, clearing all steps and active index. | void |
next | Moves to the next step, if available and valid. | void |
prev | Moves to the previous step, if available. | void |
c-stepper-step
component
import { StepperStepComponent } from '@coreui/angular-pro'