Stepper

2.3.0Beta

Use steppers to visualize a group of connected actions or the order of a workflow.

import {StepperModule} from "@qualcomm-ui/angular/stepper"

Examples

Linear Mode

The stepper is linear by default (linear is true). Steps must be completed in order.

  • Backward navigation is always allowed
  • Previously visited steps can be re-clicked without restriction
  • Forward navigation is restricted to the immediate next step
  • canGoToStep can override forward navigation decisions
First - Contact Info
<div q-stepper-root [count]="items.length">
  <div q-stepper-list>
    @for (item of items; track item.value; let i = $index) {
      <div q-stepper-item [index]="i">
        <button q-stepper-trigger>
          <div q-stepper-indicator>{{ i + 1 }}</div>
          <span q-stepper-label>{{ item.title }}</span>
        </button>
        <div q-stepper-separator></div>
      </div>
    }
  </div>

  @for (item of items; track item.value; let i = $index) {
    <div q-stepper-content [index]="i">
      {{ item.title }} - {{ item.description }}
    </div>
  }

  <div q-stepper-completed-content>All steps completed.</div>

  <div class="mt-6 flex justify-between">
    <button
      q-button
      q-stepper-prev-trigger
      size="sm"
      startIcon="ChevronLeft"
      variant="outline"
    >
      Back
    </button>
    <button
      endIcon="ChevronRight"
      q-button
      q-stepper-next-trigger
      size="sm"
      variant="outline"
    >
      Next
    </button>
  </div>
</div>

Non-linear Mode

Set linear to false to allow accessing any step at any time. Use the completed prop to manually track which steps are done.

canGoToStep applies to all navigation if provided. Otherwise, all navigation is allowed.

First - Contact Info
<div q-stepper-root [count]="items.length" [linear]="false">
  <div q-stepper-list>
    @for (item of items; track item.value; let i = $index) {
      <div q-stepper-item [index]="i">
        <button q-stepper-trigger>
          <div q-stepper-indicator>{{ i + 1 }}</div>
          <span q-stepper-label>{{ item.title }}</span>
        </button>
        <div q-stepper-separator></div>
      </div>
    }
  </div>

  @for (item of items; track item.value; let i = $index) {
    <div q-stepper-content [index]="i">
      {{ item.title }} - {{ item.description }}
    </div>
  }
</div>

Icon

Use the q-stepper-indicator-icon directive on an <svg> element to use an icon for each step's indicator.

Contact details
<div q-stepper-root [count]="items.length">
  <div q-stepper-list>
    @for (item of items; track item.value; let i = $index) {
      <div q-stepper-item [index]="i">
        <button q-stepper-trigger>
          <div q-stepper-indicator>
            <svg q-stepper-indicator-icon [qIcon]="item.icon"></svg>
          </div>
          <span q-stepper-label>{{ item.title }}</span>
        </button>
        <div q-stepper-separator></div>
      </div>
    }
  </div>

  @for (item of items; track item.value; let i = $index) {
    <div q-stepper-content [index]="i">
      {{ item.content }}
    </div>
  }

  <div q-stepper-completed-content>All steps completed.</div>

  <div class="mt-6 flex justify-between">
    <button
      q-button
      q-stepper-prev-trigger
      size="sm"
      startIcon="ChevronLeft"
      variant="outline"
    >
      Back
    </button>
    <button
      endIcon="ChevronRight"
      q-button
      q-stepper-next-trigger
      size="sm"
      variant="outline"
    >
      Next
    </button>
  </div>
</div>

Hint

Add a hint to provide additional context below the step label.

Provide your payment details
<div q-stepper-root [count]="items.length" [defaultStep]="1">
  <div q-stepper-list>
    @for (item of items; track item.value; let i = $index) {
      <div q-stepper-item [index]="i">
        <button q-stepper-trigger>
          <div q-stepper-indicator>{{ i + 1 }}</div>
          <span q-stepper-label>{{ item.title }}</span>
          <span q-stepper-hint>{{ item.hint }}</span>
        </button>
        <div q-stepper-separator></div>
      </div>
    }
  </div>

  @for (item of items; track item.value; let i = $index) {
    <div q-stepper-content [index]="i">
      {{ item.content }}
    </div>
  }

  <div q-stepper-completed-content>All steps completed.</div>

  <div class="mt-6 flex justify-between">
    <button
      q-button
      q-stepper-prev-trigger
      size="sm"
      startIcon="ChevronLeft"
      variant="outline"
    >
      Back
    </button>
    <button
      endIcon="ChevronRight"
      q-button
      q-stepper-next-trigger
      size="sm"
      variant="outline"
    >
      Next
    </button>
  </div>
</div>

Layout & Orientation

Stepper orientation and label placement are controlled through the orientation input.

Horizontal

horizontal (default): The step list is centered horizontally, and the labels are stacked vertically and centered below the step indicator.

Provide your payment details
<div q-stepper-root [count]="items.length" [defaultStep]="1">
  <div q-stepper-list>
    @for (item of items; track item.value; let i = $index) {
      <div q-stepper-item [index]="i">
        <button q-stepper-trigger>
          <div q-stepper-indicator>{{ i + 1 }}</div>
          <span q-stepper-label>{{ item.title }}</span>
          <span q-stepper-hint>{{ item.hint }}</span>
        </button>
        <div q-stepper-separator></div>
      </div>
    }
  </div>

  @for (item of items; track item.value; let i = $index) {
    <div q-stepper-content [index]="i">
      {{ item.content }}
    </div>
  }

  <div q-stepper-completed-content>All steps completed.</div>

  <div class="mt-6 flex justify-between">
    <button
      q-button
      q-stepper-prev-trigger
      size="sm"
      startIcon="ChevronLeft"
      variant="outline"
    >
      Back
    </button>
    <button
      endIcon="ChevronRight"
      q-button
      q-stepper-next-trigger
      size="sm"
      variant="outline"
    >
      Next
    </button>
  </div>
</div>

Horizontal Inline

horizontal-inline is a variant of horizontal that places the step indicator and labels inline. The step items are left-aligned and take the full width of their parent container

First - Contact Info
<div orientation="horizontal-inline" q-stepper-root [count]="items.length">
  <div q-stepper-list>
    @for (item of items; track item.value; let i = $index) {
      <div q-stepper-item [index]="i">
        <button q-stepper-trigger>
          <div q-stepper-indicator>{{ i + 1 }}</div>
          <span q-stepper-label>{{ item.title }}</span>
          <span q-stepper-hint>{{ item.description }}</span>
        </button>
        <div q-stepper-separator></div>
      </div>
    }
  </div>

  @for (item of items; track item.value; let i = $index) {
    <div q-stepper-content [index]="i">
      {{ item.title }} - {{ item.description }}
    </div>
  }

  <div q-stepper-completed-content>All steps completed.</div>

  <div class="mt-6 flex justify-between">
    <button
      q-button
      q-stepper-prev-trigger
      size="sm"
      startIcon="ChevronLeft"
      variant="outline"
    >
      Back
    </button>
    <button
      endIcon="ChevronRight"
      q-button
      q-stepper-next-trigger
      size="sm"
      variant="outline"
    >
      Next
    </button>
  </div>
</div>

Horizontal Bottom Start

horizontal-bottom-start is a variant of horizontal that places the labels below each step, aligned to the start. The items are left-aligned and take the full width of their parent container.

First - Contact Info
<div
  orientation="horizontal-bottom-start"
  q-stepper-root
  [count]="items.length"
>
  <div q-stepper-list>
    @for (item of items; track item.value; let i = $index) {
      <div q-stepper-item [index]="i">
        <button q-stepper-trigger>
          <div q-stepper-indicator>{{ i + 1 }}</div>
          <span q-stepper-label>{{ item.title }}</span>
          <span q-stepper-hint>{{ item.description }}</span>
        </button>
        <div q-stepper-separator></div>
      </div>
    }
  </div>

  @for (item of items; track item.value; let i = $index) {
    <div q-stepper-content [index]="i">
      {{ item.title }} - {{ item.description }}
    </div>
  }

  <div q-stepper-completed-content>All steps completed.</div>

  <div class="mt-6 flex justify-between">
    <button
      q-button
      q-stepper-prev-trigger
      size="sm"
      startIcon="ChevronLeft"
      variant="outline"
    >
      Back
    </button>
    <button
      endIcon="ChevronRight"
      q-button
      q-stepper-next-trigger
      size="sm"
      variant="outline"
    >
      Next
    </button>
  </div>
</div>

Vertical

Set orientation to vertical to stack the steps vertically. The labels are centered relative to the step indicator.

Contact details
<div
  class="min-h-96"
  orientation="vertical"
  q-stepper-root
  [count]="items.length"
>
  <div q-stepper-list>
    @for (item of items; track item.value; let i = $index) {
      <div q-stepper-item [index]="i">
        <button q-stepper-trigger>
          <div q-stepper-indicator>{{ i + 1 }}</div>
          <span q-stepper-label>{{ item.title }}</span>
          <span q-stepper-hint>{{ item.content }}</span>
        </button>
        <div q-stepper-separator></div>
      </div>
    }
  </div>

  <div class="flex flex-col gap-4">
    @for (item of items; track item.value; let i = $index) {
      <div q-stepper-content [index]="i">
        {{ item.content }}
      </div>
    }

    <div q-stepper-completed-content>All steps completed.</div>

    <div class="mt-6 flex gap-2">
      <button
        q-button
        q-stepper-prev-trigger
        size="sm"
        startIcon="ChevronLeft"
        variant="outline"
      >
        Back
      </button>
      <button
        endIcon="ChevronRight"
        q-button
        q-stepper-next-trigger
        size="sm"
        variant="outline"
      >
        Next
      </button>
    </div>
  </div>
</div>

Vertical Inline

Set orientation to vertical-inline to stack the steps vertically while aligning the labels inline with the step indicator.

Contact details
<div
  class="min-h-96"
  orientation="vertical-inline"
  q-stepper-root
  [count]="items.length"
>
  <div q-stepper-list>
    @for (item of items; track item.value; let i = $index) {
      <div q-stepper-item [index]="i">
        <button q-stepper-trigger>
          <div q-stepper-indicator>{{ i + 1 }}</div>
          <span q-stepper-label>{{ item.title }}</span>
          <span q-stepper-hint>{{ item.content }}</span>
        </button>
        <div q-stepper-separator></div>
      </div>
    }
  </div>

  <div class="flex flex-col gap-4">
    @for (item of items; track item.value; let i = $index) {
      <div q-stepper-content [index]="i">
        {{ item.content }}
      </div>
    }

    <div q-stepper-completed-content>All steps completed.</div>

    <div class="mt-6 flex gap-2">
      <button
        q-button
        q-stepper-prev-trigger
        size="sm"
        startIcon="ChevronLeft"
        variant="outline"
      >
        Back
      </button>
      <button
        endIcon="ChevronRight"
        q-button
        q-stepper-next-trigger
        size="sm"
        variant="outline"
      >
        Next
      </button>
    </div>
  </div>
</div>

Controlled State

Use step and (stepChanged) to control the active step externally.

Contact details
import {Component, signal} from "@angular/core"
import {ChevronLeft, ChevronRight} from "lucide-angular"

import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
import {ButtonModule} from "@qualcomm-ui/angular/button"
import {StepperModule} from "@qualcomm-ui/angular/stepper"

@Component({
  imports: [StepperModule, ButtonModule],
  providers: [provideIcons({ChevronLeft, ChevronRight})],
  selector: "stepper-controlled-demo",
  template: `
    <div
      q-stepper-root
      [count]="items.length"
      [step]="step()"
      (stepChanged)="step.set($event)"
    >
      <div q-stepper-list>
        @for (item of items; track item.value; let i = $index) {
          <div q-stepper-item [index]="i">
            <button q-stepper-trigger>
              <div q-stepper-indicator>{{ i + 1 }}</div>
              <span q-stepper-label>{{ item.title }}</span>
            </button>
            <div q-stepper-separator></div>
          </div>
        }
      </div>

      @for (item of items; track item.value; let i = $index) {
        <div q-stepper-content [index]="i">
          {{ item.content }}
        </div>
      }

      <div q-stepper-completed-content>All steps completed.</div>

      <div class="mt-6 flex justify-between">
        <button
          q-button
          q-stepper-prev-trigger
          size="sm"
          startIcon="ChevronLeft"
          variant="outline"
        >
          Back
        </button>
        <button
          endIcon="ChevronRight"
          q-button
          q-stepper-next-trigger
          size="sm"
          variant="outline"
        >
          Next
        </button>
      </div>
    </div>
  `,
})
export class StepperControlledDemo {
  readonly items = [
    {content: "Contact details", title: "Step 1", value: "step-1"},
    {content: "Payment info", title: "Step 2", value: "step-2"},
    {content: "Confirmation", title: "Step 3", value: "step-3"},
  ]
  readonly step = signal(0)
}

Manual Completion

Set linear to false and use the completed input to manually control which steps are marked as complete.

Account content
import {Component, computed, signal} from "@angular/core"
import {ChevronLeft, ChevronRight} from "lucide-angular"

import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
import {ButtonModule} from "@qualcomm-ui/angular/button"
import {StepperModule} from "@qualcomm-ui/angular/stepper"

@Component({
  imports: [StepperModule, ButtonModule],
  providers: [provideIcons({ChevronLeft, ChevronRight})],
  selector: "stepper-completed-demo",
  template: `
    <div
      q-stepper-root
      [completed]="completed()"
      [count]="items.length"
      [linear]="false"
    >
      <div q-stepper-list>
        @for (item of items; track item.value; let i = $index) {
          <div q-stepper-item [index]="i">
            <button q-stepper-trigger>
              <div q-stepper-indicator>{{ i + 1 }}</div>
              <span q-stepper-label>{{ item.title }}</span>
            </button>
            <div q-stepper-separator></div>
          </div>
        }
      </div>

      @for (item of items; track item.value; let i = $index) {
        <div q-stepper-content [index]="i">
          <div class="flex items-center gap-4">
            <span>{{ item.title }} content</span>
            <button
              q-button
              size="sm"
              [variant]="completed()[i] ? 'outline' : 'fill'"
              (click)="toggleCompleted(i)"
            >
              {{ completed()[i] ? "Mark Incomplete" : "Mark Complete" }}
            </button>
          </div>
        </div>
      }

      <div q-stepper-completed-content>All steps completed.</div>

      <div class="mt-6 flex justify-between">
        <button
          q-button
          q-stepper-prev-trigger
          size="sm"
          startIcon="ChevronLeft"
          variant="outline"
        >
          Back
        </button>
        <ng-container *stepperContext="let api">
          <button
            endIcon="ChevronRight"
            q-button
            q-stepper-next-trigger
            size="sm"
            variant="outline"
            [disabled]="
              !api.hasNextStep ||
              (api.step === api.count - 1 && !allCompleted())
            "
          >
            Next
          </button>
        </ng-container>
      </div>
    </div>
  `,
})
export class StepperCompletedDemo {
  readonly items = [
    {title: "Account", value: "account"},
    {title: "Profile", value: "profile"},
    {title: "Review", value: "review"},
  ]
  readonly completed = signal<Record<number, boolean>>({})

  readonly allCompleted = computed(() => {
    const completed = this.completed()
    return this.items.every((_, index) => completed[index])
  })

  toggleCompleted(index: number) {
    this.completed.update((prev) => ({...prev, [index]: !prev[index]}))
  }
}

Pending Steps

Use the pending input to mark steps as pending. This is typically used to indicate that a step has been visited but not completed.

Account content
<div q-stepper-root [count]="items.length" [pending]="pending()">
  <div q-stepper-list>
    @for (item of items; track item.value; let i = $index) {
      <div q-stepper-item [index]="i">
        <button q-stepper-trigger>
          <div q-stepper-indicator>{{ i + 1 }}</div>
          <span q-stepper-label>{{ item.title }}</span>
        </button>
        <div q-stepper-separator></div>
      </div>
    }
  </div>

  @for (item of items; track item.value; let i = $index) {
    <div q-stepper-content [index]="i">
      <div class="flex items-center gap-4">
        <span>{{ item.title }} content</span>
      </div>
    </div>
  }

  <div q-stepper-completed-content>All steps completed.</div>

  <ng-container *stepperContext="let api">
    <div class="mt-6 flex justify-between">
      <button
        q-button
        q-stepper-prev-trigger
        size="sm"
        startIcon="ChevronLeft"
        variant="outline"
      >
        Back
      </button>
      <button
        endIcon="ChevronRight"
        q-button
        q-stepper-next-trigger
        size="sm"
        variant="outline"
        (click)="pendingStep.set(api.step + 1)"
      >
        Next
      </button>
    </div>
  </ng-container>
</div>

Sizes

The stepper comes in two sizes: sm and lg. The default size is lg.

Contact details
Contact details
import {Component} from "@angular/core"

import {StepperModule} from "@qualcomm-ui/angular/stepper"

@Component({
  imports: [StepperModule],
  selector: "stepper-sizes-demo",
  template: `
    <div class="flex w-full flex-col gap-16">
      @for (size of sizes; track size) {
        <div q-stepper-root [count]="items.length" [size]="size">
          <div q-stepper-list>
            @for (item of items; track item.value; let i = $index) {
              <div q-stepper-item [index]="i">
                <button q-stepper-trigger>
                  <div q-stepper-indicator>{{ i + 1 }}</div>
                  <span q-stepper-label>{{ item.title }}</span>
                </button>
                <div q-stepper-separator></div>
              </div>
            }
          </div>

          @for (item of items; track item.value; let i = $index) {
            <div q-stepper-content [index]="i">
              {{ item.content }}
            </div>
          }
        </div>
      }
    </div>
  `,
})
export class StepperSizesDemo {
  readonly items = [
    {content: "Contact details", title: "Step 1", value: "step-1"},
    {content: "Payment info", title: "Step 2", value: "step-2"},
    {content: "Confirmation", title: "Step 3", value: "step-3"},
  ]
  readonly sizes = ["sm", "lg"] as const
}

Validation Examples

Non-linear Form

Combine linear={false} with per-step validation to let users complete steps in any order. Use the invalid and completed props to track each step's status, and stepperContext to conditionally render a submit action once all steps pass.

import {Component, inject, signal} from "@angular/core"
import {FormBuilder, ReactiveFormsModule, Validators} from "@angular/forms"
import {ChevronLeft, ChevronRight} from "lucide-angular"

import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
import {ButtonModule} from "@qualcomm-ui/angular/button"
import {StepperModule} from "@qualcomm-ui/angular/stepper"
import {TextInputModule} from "@qualcomm-ui/angular/text-input"

@Component({
  imports: [StepperModule, ButtonModule, TextInputModule, ReactiveFormsModule],
  providers: [provideIcons({ChevronLeft, ChevronRight})],
  selector: "stepper-nonlinear-form-demo",
  template: `
    <div
      q-stepper-root
      [completed]="completed()"
      [count]="items.length"
      [invalid]="invalid()"
      [linear]="false"
    >
      <div q-stepper-list>
        @for (item of items; track item.name; let i = $index) {
          <div q-stepper-item [index]="i">
            <button q-stepper-trigger>
              <div q-stepper-indicator>{{ i + 1 }}</div>
              <span q-stepper-label>{{ item.title }}</span>
            </button>
            <div q-stepper-separator></div>
          </div>
        }
      </div>

      @for (item of items; track item.name; let i = $index) {
        <div q-stepper-content [index]="i">
          <q-text-input
            class="w-72"
            [errorText]="getErrorText(item.name)"
            [formControl]="form.controls[item.name]"
            [invalid]="isFieldInvalid(item.name)"
            [label]="item.label"
            [placeholder]="item.placeholder"
          />
        </div>
      }

      <div q-stepper-completed-content>
        Survey submitted. Thank you for your feedback!
      </div>

      <div class="mt-6 flex justify-between">
        <ng-container *stepperContext="let api">
          <button
            q-button
            q-stepper-prev-trigger
            size="sm"
            startIcon="ChevronLeft"
            variant="outline"
            (click)="saveStep(api.step)"
          >
            Back
          </button>

          @if (allCompleted()) {
            <button
              endIcon="ChevronRight"
              q-button
              size="sm"
              (click)="saveStep(api.step); api.goToNextStep()"
            >
              Submit
            </button>
          } @else {
            <button
              endIcon="ChevronRight"
              q-button
              q-stepper-next-trigger
              size="sm"
              [disabled]="
                !api.hasNextStep ||
                (api.step === items.length - 1 && !allCompleted())
              "
              (click)="saveStep(api.step)"
            >
              Next
            </button>
          }
        </ng-container>
      </div>
    </div>
  `,
})
export class StepperNonlinearFormDemo {
  readonly items = [
    {
      label: "Age range",
      name: "age" as const,
      placeholder: "25-34",
      title: "Demographics",
    },
    {
      label: "Preferred contact method",
      name: "contact" as const,
      placeholder: "Email",
      title: "Preferences",
    },
    {
      label: "Comments",
      name: "comments" as const,
      placeholder: "Tell us what you think",
      title: "Feedback",
    },
  ]
  readonly completed = signal<Record<number, boolean>>({})
  readonly invalid = signal<Record<number, boolean>>({})

  private fb = inject(FormBuilder)

  readonly form = this.fb.group({
    age: ["", Validators.required],
    comments: ["", Validators.required],
    contact: ["", Validators.required],
  })

  allCompleted(): boolean {
    return this.items.every((_, i) => this.completed()[i])
  }

  isFieldInvalid(name: "age" | "comments" | "contact"): boolean {
    const control = this.form.controls[name]
    return control.invalid && (control.dirty || control.touched)
  }

  getErrorText(name: "age" | "comments" | "contact"): string {
    const control = this.form.controls[name]
    if (control.hasError("required")) {
      const item = this.items.find((i) => i.name === name)
      return `${item?.label} is required`
    }
    return ""
  }

  saveStep(index: number) {
    const field = this.items[index]?.name
    if (!field) {
      return
    }

    const control = this.form.controls[field]
    control.markAsTouched()

    if (control.valid) {
      this.invalid.update((prev) => ({...prev, [index]: false}))
      this.completed.update((prev) => ({...prev, [index]: true}))
    } else {
      this.invalid.update((prev) => ({...prev, [index]: true}))
      this.completed.update((prev) => ({...prev, [index]: false}))
    }
  }
}

Skippable Steps

Use isStepSkippable to mark optional steps that can be bypassed during navigation.

import {Component, inject, signal} from "@angular/core"
import {FormBuilder, ReactiveFormsModule, Validators} from "@angular/forms"
import {ChevronLeft, ChevronRight} from "lucide-angular"

import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
import {ButtonModule} from "@qualcomm-ui/angular/button"
import {StepperModule} from "@qualcomm-ui/angular/stepper"
import {TextInputModule} from "@qualcomm-ui/angular/text-input"
import type {
  CanGoToStepDetails,
  StepInvalidDetails,
} from "@qualcomm-ui/core/stepper"

const PROMO_STEP = 1

const steps = [
  {title: "Shipping", value: "shipping"},
  {title: "Promo Code", value: "promo"},
  {title: "Payment", value: "payment"},
]

@Component({
  imports: [StepperModule, ButtonModule, TextInputModule, ReactiveFormsModule],
  providers: [provideIcons({ChevronLeft, ChevronRight})],
  selector: "stepper-skippable-steps-demo",
  template: `
    <div
      q-stepper-root
      [canGoToStep]="canGoToStep"
      [count]="steps.length"
      [isStepSkippable]="isStepSkippable"
      [step]="step()"
      (stepChanged)="step.set($event)"
      (stepInvalid)="onStepInvalid($event)"
    >
      <div q-stepper-list>
        @for (s of steps; track s.value; let i = $index) {
          <div q-stepper-item [index]="i">
            <button q-stepper-trigger>
              <div q-stepper-indicator>{{ i + 1 }}</div>
              <span q-stepper-label>
                {{ s.title }}
                @if (i === promoStep) {
                  <span q-stepper-hint>(optional)</span>
                }
              </span>
            </button>
            <div q-stepper-separator></div>
          </div>
        }
      </div>

      <div q-stepper-content [index]="0">
        <q-text-input
          class="max-w-56"
          label="Shipping address"
          placeholder="123 Main St"
          [errorText]="getErrorText('address')"
          [formControl]="form.controls.address"
          [invalid]="isFieldInvalid('address')"
        />
      </div>

      <div q-stepper-content [index]="1">
        <q-text-input
          class="max-w-56"
          label="Promo code"
          placeholder="SAVE20"
          [formControl]="form.controls.promo"
        />
      </div>

      <div q-stepper-content [index]="2">
        <q-text-input
          class="max-w-56"
          label="Card number"
          placeholder="4242 4242 4242 4242"
          [errorText]="getErrorText('card')"
          [formControl]="form.controls.card"
          [invalid]="isFieldInvalid('card')"
        />
      </div>

      <div q-stepper-completed-content>
        Order confirmed. Thank you for your purchase!
      </div>

      <div class="mt-6 flex justify-between">
        <button
          q-button
          q-stepper-prev-trigger
          size="sm"
          startIcon="ChevronLeft"
          variant="outline"
        >
          Back
        </button>
        <button
          endIcon="ChevronRight"
          q-button
          q-stepper-next-trigger
          size="sm"
          variant="outline"
        >
          Next
        </button>
      </div>
    </div>
  `,
})
export class StepperSkippableStepsDemo {
  readonly steps = steps
  readonly promoStep = PROMO_STEP
  readonly step = signal(0)

  private fb = inject(FormBuilder)

  readonly form = this.fb.group({
    address: ["", Validators.required],
    card: ["", Validators.required],
    promo: [""],
  })

  readonly isStepSkippable = (index: number): boolean => index === PROMO_STEP

  readonly canGoToStep = ({
    current,
    target,
  }: CanGoToStepDetails): boolean | undefined => {
    if (target <= current) {
      return undefined
    }
    return this.validateStep(current)
  }

  isFieldInvalid(name: "address" | "card" | "promo"): boolean {
    const control = this.form.controls[name]
    return control.invalid && (control.dirty || control.touched)
  }

  getErrorText(name: "address" | "card"): string {
    const control = this.form.controls[name]
    if (control.hasError("required")) {
      return `${name === "address" ? "Shipping address" : "Card number"} is required`
    }
    return ""
  }

  onStepInvalid({step}: StepInvalidDetails) {
    if (step === 0) {
      this.form.controls.address.markAsTouched()
      this.form.controls.address.markAsDirty()
    }
    if (step === 2) {
      this.form.controls.card.markAsTouched()
      this.form.controls.card.markAsDirty()
    }
  }

  private validateStep(index: number): boolean {
    if (index === 0) {
      return this.form.controls.address.valid
    }
    if (index === 2) {
      return this.form.controls.card.valid
    }
    return true
  }
}

Explorer

API

q-stepper-root

PropTypeDefault
The total number of steps
number
Whether navigation to a step should be allowed. Receives the current step, target step, and whether the target has been previously visited.

Return
false to block navigation, true to allow it, or undefined to defer to the built-in navigation rules.
(details: {
current: number
target: number
visited: boolean
}) => boolean
A map of step indices to their completion status. In linear mode, steps before the current step are automatically completed.
Record<
number,
boolean
>
The initial value of the stepper when rendered. Use when you don't need to control the value of the stepper.
number
The document's text/writing direction.
'ltr' | 'rtl'
"ltr"
A root node to correctly resolve the Document in custom environments.
() =>
| Node
| ShadowRoot
| Document
id attribute. If omitted, a unique identifier will be generated for accessibility.
string
A map of step indices to their invalid status.
Record<
number,
boolean
>
Whether a step can be skipped during navigation in linear mode.
(
index: number,
) => boolean
() => false
If true, the stepper requires the user to complete the steps in order.
boolean
true
The orientation of the stepper
| 'horizontal'
| 'horizontal-inline'
| 'horizontal-bottom-start'
| 'vertical'
| 'vertical-inline'
"horizontal"
A map of step indices to their pending status.
Record<
number,
boolean
>
The size of the stepper and its elements.
'sm' | 'lg'
'lg'
The controlled value of the stepper
number
Callback to be called when the value changes
number
Called when navigation is blocked due to an invalid step.
{
action: 'set' | 'next'
step: number
targetStep?: number
}
Type
number
Description
The total number of steps
Type
(details: {
current: number
target: number
visited: boolean
}) => boolean
Description
Whether navigation to a step should be allowed. Receives the current step, target step, and whether the target has been previously visited.

Return
false to block navigation, true to allow it, or undefined to defer to the built-in navigation rules.
Type
Record<
number,
boolean
>
Description
A map of step indices to their completion status. In linear mode, steps before the current step are automatically completed.
Type
number
Description
The initial value of the stepper when rendered. Use when you don't need to control the value of the stepper.
Type
'ltr' | 'rtl'
Description
The document's text/writing direction.
Type
() =>
| Node
| ShadowRoot
| Document
Description
A root node to correctly resolve the Document in custom environments.
Type
string
Description
id attribute. If omitted, a unique identifier will be generated for accessibility.
Type
Record<
number,
boolean
>
Description
A map of step indices to their invalid status.
Type
(
index: number,
) => boolean
Description
Whether a step can be skipped during navigation in linear mode.
Type
boolean
Description
If true, the stepper requires the user to complete the steps in order.
Type
| 'horizontal'
| 'horizontal-inline'
| 'horizontal-bottom-start'
| 'vertical'
| 'vertical-inline'
Description
The orientation of the stepper
Type
Record<
number,
boolean
>
Description
A map of step indices to their pending status.
Type
'sm' | 'lg'
Description
The size of the stepper and its elements.
Type
number
Description
The controlled value of the stepper
Type
number
Description
Callback to be called when the value changes
Type
{
action: 'set' | 'next'
step: number
targetStep?: number
}
Description
Called when navigation is blocked due to an invalid step.

q-stepper-list

PropType
id attribute. If omitted, a unique identifier will be generated for accessibility.
string
Type
string
Description
id attribute. If omitted, a unique identifier will be generated for accessibility.

q-stepper-item

PropType
The index of the step
number
Type
number
Description
The index of the step

q-stepper-trigger

PropType
id attribute. If omitted, a unique identifier will be generated for accessibility.
string
Type
string
Description
id attribute. If omitted, a unique identifier will be generated for accessibility.

q-stepper-indicator

PropTypeDefault
Icon to display when the step is completed.
| LucideIconData
| string
'check'
Icon to display when the step is in an error state.
| LucideIconData
| string
StepperIndicatorAlert
Type
| LucideIconData
| string
Description
Icon to display when the step is completed.
Type
| LucideIconData
| string
Description
Icon to display when the step is in an error state.

q-stepper-separator

q-stepper-completed-content

Content displayed when all steps are completed.

q-stepper-content

PropType
The index of the step this content belongs to
number
id attribute. If omitted, a unique identifier will be generated for accessibility.
string
Type
number
Description
The index of the step this content belongs to
Type
string
Description
id attribute. If omitted, a unique identifier will be generated for accessibility.

q-stepper-label

q-stepper-hint

q-stepper-next-trigger

q-stepper-prev-trigger

stepperContext

Access the API of the StepperRootDirective in the template.

StepperApi

PropType
The total number of steps.
number
Returns the resolved state for a step at the given index.
    (props: {
    index: number
    }) => {
    completed: boolean
    contentId: string
    current: boolean
    first: boolean
    incomplete: boolean
    index: number
    invalid: boolean
    last: boolean
    pending?: boolean
    previous?: boolean
    skippable: boolean
    triggerId: string
    visited: boolean
    }
    Function to go to the next step.
      () => void
      Function to go to the previous step.
        () => void
        Whether the stepper has a next step.
        boolean
        Whether the stepper has a previous step.
        boolean
        Check if a specific step can be skipped
          (
          index: number,
          ) => boolean
          Function to go to reset the stepper.
            () => void
            Function to set the value of the stepper.
              (
              step: number,
              ) => void
              The value of the stepper.
              number
              Type
              number
              Description
              The total number of steps.
              Type
              (props: {
              index: number
              }) => {
              completed: boolean
              contentId: string
              current: boolean
              first: boolean
              incomplete: boolean
              index: number
              invalid: boolean
              last: boolean
              pending?: boolean
              previous?: boolean
              skippable: boolean
              triggerId: string
              visited: boolean
              }
              Description
              Returns the resolved state for a step at the given index.
                Type
                () => void
                Description
                Function to go to the next step.
                  Type
                  () => void
                  Description
                  Function to go to the previous step.
                    Type
                    boolean
                    Description
                    Whether the stepper has a next step.
                    Type
                    boolean
                    Description
                    Whether the stepper has a previous step.
                    Type
                    (
                    index: number,
                    ) => boolean
                    Description
                    Check if a specific step can be skipped
                      Type
                      () => void
                      Description
                      Function to go to reset the stepper.
                        Type
                        (
                        step: number,
                        ) => void
                        Description
                        Function to set the value of the stepper.
                          Type
                          number
                          Description
                          The value of the stepper.
                          Last updated on by Ryan Bower