<bpdm/ui />
Overlay

Step Dialog

A multi-step wizard dialog — progress stepper with Back/Next/Finish, in React (@bpdm/ui) and Angular (@bpdm/ng).

The Step Dialog is a multi-step "wizard" — a progress stepper, per-step content, and Back / Next / Finish navigation, built on the Dialog. Pass an array of steps; the current step resets when the dialog closes, and onComplete (React) / (complete) (Angular) fires on Finish. In Angular it's <bpdm-step-dialog> with per-step <ng-template>s.

Usage

Each step has a title, optional description, and content. The footer shows Back / Next automatically, and Finish on the last step.

setup-wizard.tsx
import { StepDialog, type StepDialogStep } from '@bpdm/ui/step-dialog';
import { Button } from '@bpdm/ui/button';
import { Input } from '@bpdm/ui/input';

const steps: StepDialogStep[] = [
  {
    title: 'Account',
    description: 'Your login details.',
    content: <Input type="email" placeholder="you@company.com" aria-label="Email" />,
  },
  {
    title: 'Profile',
    description: 'Tell us about you.',
    content: <Input placeholder="Display name" aria-label="Display name" />,
  },
  {
    title: 'Done',
    description: 'Review and finish.',
    content: <p>Everything looks good — click Finish to complete setup.</p>,
  },
];

export function SetupWizard() {
  return (
    <StepDialog
      trigger={<Button>Get started</Button>}
      title="Set up workspace"
      steps={steps}
      onComplete={() => {
        // …persist the wizard result…
      }}
    />
  );
}

In Angular each step's content is an <ng-template>; reference them with viewChild and build the steps array.

setup-wizard.ts
import { Component, computed, viewChild, type TemplateRef } from '@angular/core';
import { BpdmStepDialog, BpdmStepDialogTrigger, BpdmButton, BpdmInput } from '@bpdm/ng';

@Component({
  selector: 'setup-wizard',
  imports: [BpdmStepDialog, BpdmStepDialogTrigger, BpdmButton, BpdmInput],
  templateUrl: './setup-wizard.html',
})
export class SetupWizard {
  private readonly accountTpl = viewChild.required<TemplateRef<unknown>>('accountTpl');
  private readonly profileTpl = viewChild.required<TemplateRef<unknown>>('profileTpl');
  private readonly doneTpl = viewChild.required<TemplateRef<unknown>>('doneTpl');

  readonly steps = computed(() => [
    { title: 'Account', description: 'Your login details.', content: this.accountTpl() },
    { title: 'Profile', description: 'Tell us about you.', content: this.profileTpl() },
    { title: 'Done', description: 'Review and finish.', content: this.doneTpl() },
  ]);

  onComplete() {
    // …persist the wizard result…
  }
}
setup-wizard.html
<bpdm-step-dialog title="Set up workspace" [steps]="steps()" (complete)="onComplete()">
  <button bpdmButton bpdmStepDialogTrigger>Get started</button>
</bpdm-step-dialog>

<ng-template #accountTpl>
  <input bpdmInput type="email" placeholder="you@company.com" aria-label="Email" />
</ng-template>
<ng-template #profileTpl>
  <input bpdmInput placeholder="Display name" aria-label="Display name" />
</ng-template>
<ng-template #doneTpl>
  <p>Everything looks good — click Finish to complete setup.</p>
</ng-template>

Internationalization

The step dialog carries a little navigation copy of its own — the Back / Next / Finish button labels, the screen-reader progress text, and the close button's accessible name. Set them once via a single messages object; it's a Partial<StepDialogMessages> merged over the English defaults, so override one key or all five:

  • back — Back button label. Default "Back".

  • next — Next button label. Default "Next".

  • finish — Finish button label. Default "Finish". A per-instance finishText prop still overrides this.

  • step — the visually-hidden progress text. Default "Step {index} of {total}"; the {index} and {total} tokens are interpolated with the live position.

  • close — the inherited Dialog close (X) button's accessible name (aria-label). Default "Close".

  • React — pass messages to <StepDialog messages={{ … }} />.

  • Angular — bind [messages] on <bpdm-step-dialog>.

Being built on the Dialog, it mirrors under dir="rtl" with no extra prop — the stepper, the footer buttons, and the close (X) button all flip to the inline-start side.

The example below localises the built-in copy to German via messages — the Back / Next / Finish labels, the progress text, and the close button — while your own step content stays untouched:

step-dialog-i18n.tsx
import { StepDialog, type StepDialogStep, type StepDialogMessages } from '@bpdm/ui/step-dialog';
import { Button } from '@bpdm/ui/button';
import { Input } from '@bpdm/ui/input';

// German labels for the built-in Back / Next / Finish / progress / close copy.
const messages: Partial<StepDialogMessages> = {
  back: 'Zurück',
  next: 'Weiter',
  finish: 'Fertig',
  step: 'Schritt {index} von {total}',
  close: 'Schließen',
};

const steps: StepDialogStep[] = [
  {
    title: 'Account',
    description: 'Your login details.',
    content: <Input type="email" placeholder="you@company.com" aria-label="Email" />,
  },
  {
    title: 'Profile',
    description: 'Tell us about you.',
    content: <Input placeholder="Display name" aria-label="Display name" />,
  },
  {
    title: 'Done',
    description: 'Review and finish.',
    content: <p>Everything looks good — finish to complete setup.</p>,
  },
];

export function SetupWizard() {
  return (
    <StepDialog
      trigger={<Button>Get started</Button>}
      title="Set up workspace"
      steps={steps}
      messages={messages}
      onComplete={() => {
        // save the result here
      }}
    />
  );
}
setup-wizard.ts
import { Component, computed, viewChild, type TemplateRef } from '@angular/core';
import {
  BpdmStepDialog,
  BpdmStepDialogTrigger,
  BpdmButton,
  BpdmInput,
  type StepDialogMessages,
} from '@bpdm/ng';

@Component({
  selector: 'setup-wizard',
  imports: [BpdmStepDialog, BpdmStepDialogTrigger, BpdmButton, BpdmInput],
  templateUrl: './setup-wizard.html',
})
export class SetupWizard {
  // German labels for the built-in Back / Next / Finish / progress / close copy.
  readonly messages: Partial<StepDialogMessages> = {
    back: 'Zurück',
    next: 'Weiter',
    finish: 'Fertig',
    step: 'Schritt {index} von {total}',
    close: 'Schließen',
  };

  private readonly accountTpl = viewChild.required<TemplateRef<unknown>>('accountTpl');
  private readonly profileTpl = viewChild.required<TemplateRef<unknown>>('profileTpl');
  private readonly doneTpl = viewChild.required<TemplateRef<unknown>>('doneTpl');

  readonly steps = computed(() => [
    { title: 'Account', description: 'Your login details.', content: this.accountTpl() },
    { title: 'Profile', description: 'Tell us about you.', content: this.profileTpl() },
    { title: 'Done', description: 'Review and finish.', content: this.doneTpl() },
  ]);

  onComplete() {
    // save the result here
  }
}
setup-wizard.html
<bpdm-step-dialog
  title="Set up workspace"
  [steps]="steps()"
  [messages]="messages"
  (complete)="onComplete()"
>
  <button bpdmButton bpdmStepDialogTrigger>Get started</button>
</bpdm-step-dialog>

<ng-template #accountTpl>
  <input bpdmInput type="email" placeholder="you@company.com" aria-label="Email" />
</ng-template>
<ng-template #profileTpl>
  <input bpdmInput placeholder="Display name" aria-label="Display name" />
</ng-template>
<ng-template #doneTpl>
  <p>Everything looks good — finish to complete setup.</p>
</ng-template>

API

PropTypeDefaultDescription
stepsStepDialogStep[]Required. { title, description?, content } per step.
triggerReactNodeElement that opens it (React). Angular: the [bpdmStepDialogTrigger] directive on your trigger.
titleReactNodecurrent step's titleOverall dialog heading. Angular: [title].
sizesm | md | lg | xlmdPanel width. Angular: [size].
open / defaultOpenbooleanControlled / uncontrolled open. Angular: two-way [(open)].
onOpenChange(open: boolean) => voidReact — fires on open/close. Angular: covered by [(open)].
onComplete() => voidReact — fires on Finish. Angular: (complete).
finishTextstringFinishLabel of the last-step button (overrides messages.finish). Angular: [finishText].
messagesPartial<StepDialogMessages>EnglishOverride the navigation / a11y strings for i18n (see below). Angular: [messages].

StepDialogStep: { title: string; description?: string; content: ReactNode } (React) / content: TemplateRef (Angular).

App-wide copy — messages

Localise the navigation and accessibility strings in one place (see Internationalization). It's a Partial<StepDialogMessages> merged over the English defaults — override one key or all five.

  • React — the messages prop on <StepDialog>.
  • Angular — the [messages] input on <bpdm-step-dialog>.
StepDialogMessages keyDefaultDescription
backBackBack button label.
nextNextNext button label.
finishFinishFinish button label (per-instance finishText overrides).
stepStep {index} of {total}Visually-hidden progress text; {index} / {total} are interpolated.
closeCloseAccessible name (aria-label) for the inherited Dialog close (X) button.

Accessibility

  • It's a modal built on the Dialogrole="dialog" + aria-modal="true", with a focus trap and scroll lock while it's open.
  • The dialog title — which defaults to the current step's title — is the dialog's accessible name (aria-labelledby); the step's description, when set, is linked via aria-describedby.
  • The progress stepper is an ordered list (<ol>): the active step carries aria-current="step", completed steps show a check, and a visually-hidden "Step N of M" position (from messages.step) is announced via a polite live region — so progress is conveyed to assistive tech, not by colour or the check glyph alone.
  • Back / Next / Finish are real buttons with full keyboard and focus support, labelled by their (translatable) text (messages.back / messages.next, and finishText or messages.finish).
  • The close (X) button carries an accessible name via messages.close (default "Close").
  • Focus moves into the dialog on open and returns to the element that opened it — the trigger — on close; the step resets to the first step each time it reopens.
  • The per-step content animates in, but conveys no meaning through motion alone.
  • All copy — Back, Next, Finish, the progress text, and the close label — is translatable via messages, and everything mirrors correctly under dir="rtl".

On this page