Angular Stepper

Stepper

CoreUI PRO
This component is part of CoreUI PRO – a powerful UI library with over 250 components and 25+ templates, designed to help you build modern, responsive apps faster. Fully compatible with Angular, Bootstrap, React.js, and Vue.js.

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:

Added in v5.5.2

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">&#64;</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">&#64;</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">&#64;</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">&#64;</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">&#64;</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 like aria-selected, aria-controls, and tabindex.
  • 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:

KeyFunctionNote
ArrowLeftMoves focus to previous steplayout=“horizontal”
ArrowRightMoves focus to next step
ArrowUpMoves focus to previous steplayout=“vertical”
ArrowDownMoves focus to next step
HomeMoves focus to the first step[linear]=“false”
EndMoves 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.

scss
.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

scss
$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

ts
import { NgModule } from '@angular/core';
import { StepperModule } from '@coreui/angular';

@NgModule({
  imports: [StepperModule]
})
export class CustomAppModule {}

Stepper Standalone

ts
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


jsx
import { StepperComponent } from '@coreui/angular-pro'

Props

PropertyDefaultType
activeStepIndexundefinednumber

Currently active step index. When not set, falls back to defaultActiveStepIndex (0).

defaultActiveStepIndex0number

Initial active step index

id'stepper-<nextId>'string

Unique identifier for the Stepper component. If not provided, a default ID will be generated.

layout'horizontal''horizontal', 'vertical'

Layout orientation - 'horizontal': Step indicators and content are placed horizontally (default). - 'vertical': Step indicators and content are stacked vertically. Choose 'vertical' layout for mobile or narrow designs.

lineartrueboolean

Enforces linear progression (cannot skip steps). - true: Users must complete steps sequentially. - false: Users can jump freely between steps.

stepButtonLayout'horizontal''horizontal', 'vertical'

Layout of the step indicator (icon and label) - 'vertical' – Places the label below the indicator icon. - 'horizontal' – Places the label beside the indicator icon (default). This prop has no effect when layout="vertical" is used.

validationfalseboolean

Enforces validation of steps. Each step must be valid before advancing to the next.

Events

Event name
activeStepIndexChange

Event fired when the active step changes in the Angular Stepper component.

  • $event number
finished

Event fired when the user completes the last step of the Form Wizard. Use this to trigger a submit action or redirect after the final step.

  • $event boolean
onReset

Event fired when the user triggers the reset() action. Use this to reset or clear related form data.

  • $event void
Methods
namedescriptiontype
finishTriggers the finish action, marking the stepper as completed.void
resetResets the stepper to its initial state, clearing all steps and active index.void
nextMoves to the next step, if available and valid.void
prevMoves to the previous step, if available.void

c-stepper-step

component


jsx
import { StepperStepComponent } from '@coreui/angular-pro'
PropertyDefaultType
disabledfalseboolean

Whether the step is disabled.

idundefinedstring

Unique identifier for the step.

indicator(step index will be used if not provided)string, TemplateRef<any>

Optional indicator to display in the step. Can be a string or a TemplateRef for custom icons.

indicatorCtxundefinedunknown

Context for the indicator template. If indicator is a TemplateRef, this context will be passed to it.

label-string, TemplateRef<any>

Label for the step, which is required. Can be a string or a TemplateRef for custom labels.

labelCtxundefinedunknown

Context for the label template. If label is a TemplateRef, this context will be passed to it.

validundefinedboolean, null

Whether the step is valid. This can be used to indicate if the step has been completed successfully.