Skip to main content
Version: 2.4

Onboarding

The tSM onboarding library is a guided tour framework for application flows. It can highlight real UI elements, render an explanatory overlay, wait for real user interaction, preserve progress, and continue across route changes when the host is placed in the application layout.

Use onboarding for flows such as:

  • first-run walkthroughs of the application shell,
  • tutorials for complex modules,
  • release notes that point at new UI controls,
  • guided setup flows that must wait for a click, focus, input, or hover.

Runtime Architecture

The library is exported from @tsm/framework/onboarding.

The important pieces are:

PiecePurpose
OnboardingServiceStarts and stops onboarding runs. It also exposes the active run and runtime state as signals.
OnboardingRootHostComponentLayout-level host that renders the backdrop, target highlight, and overlay card.
OnboardingHostComponentRuns the scenario engine, resolves overlay content, listens to document events, and emits state changes.
OnboardingScenarioRegistrationRegisters a scenario, its custom components/templates, guards, and optional adapter.
OnboardingProgressAdapterPersists or ignores progress. The default adapter stores progress in localStorage.
tsmPreventOverlayCloseDirective for controls inside the onboarding overlay/card that must not close application overlays by bubbling click/pointer events.

In the standard TSM layout the root host is already mounted in:

<tsm-onboarding-root-host></tsm-onboarding-root-host>

Because the host lives in the application layout, a scenario can survive router navigation. Do not host it inside a feature page if the tour should continue after leaving that page.

Basic Setup

Global onboarding options are registered with provideOnboarding.

import { inject } from "@angular/core";
import { getTenantId } from "@tsm/framework/functions";
import { provideOnboarding } from "@tsm/framework/onboarding";
import { RuntimeService } from "@tsm/runtime-info";

export const appProviders = [
provideOnboarding({
progressKeyBuilder: ({ scenarioId, flowId }) => {
const runtime = inject(RuntimeService);

return [
getTenantId() ?? "default",
runtime.getCurrent()?.id ?? "anonymous",
scenarioId,
flowId,
].join(":");
},
}),
];

The progress key builder runs inside an Angular injection context when the default local-storage adapter is used, so it can call inject(). Put user, tenant, module, or entity identifiers into the key when the same scenario can run independently for different contexts.

The default key is:

`${scenarioId}:${flowId}`;

Progress Key Builder

The progress key decides where the default LocalStorageOnboardingProgressAdapter stores a snapshot. It receives only onboarding identifiers:

interface OnboardingProgressKeyContext {
scenarioId: string;
scenarioVersion: number;
flowId: string;
}

Anything else, such as the current user, tenant, application, entity, or module, must be resolved by the builder itself. The builder can call inject() because the default adapter executes it inside Angular's injection context.

import { inject } from "@angular/core";
import { getTenantId } from "@tsm/framework/functions";
import { provideOnboarding } from "@tsm/framework/onboarding";
import { RuntimeService } from "@tsm/runtime-info";

export const appProviders = [
provideOnboarding({
progressKeyBuilder: ({ scenarioId, scenarioVersion, flowId }) => {
const runtime = inject(RuntimeService);

return [
"tsm-onboarding",
getTenantId() ?? "default",
runtime.getCurrent()?.id ?? "anonymous",
scenarioId,
scenarioVersion,
flowId,
].join(":");
},
}),
];

Use flowId for runtime separation of the same scenario. For example, a single scenario id can be reused for many user tasks:

this.onboarding.start("ticket-detail-tour", {
flowId: `ticket:${ticketId}`,
});

With the key builder above, the stored progress is unique for:

  • tenant,
  • user,
  • scenario id,
  • scenario version,
  • runtime flow id.

If a scenario uses a custom OnboardingProgressAdapter, that adapter owns its own keying rules. The global progressKeyBuilder is used only by the default local-storage adapter unless the custom adapter explicitly calls it.

Injection Context

Onboarding intentionally exposes injection context in places where scenario configuration needs application services.

PlaceCan call inject()Injector used
progressKeyBuilder passed to provideOnboardingYesApplication EnvironmentInjector used by OnboardingService.
scenario factory in OnboardingScenarioRegistrationYesstart(..., {injector}) when provided, otherwise the OnboardingService injector.
preActions and postActionsYesSame action injector as the scenario factory. The default host also runs them inside NgZone.
custom step componentYesNormal Angular component injection.
guard predicate invocationNo direct injection contextCapture injected services in a factory/component first, then close over them in the guard.

This is useful when a lazy feature owns route-scoped services. Start the scenario with the feature injector when the factory or step actions must see those providers:

import { Injector, inject } from "@angular/core";
import { OnboardingService } from "@tsm/framework/onboarding";

export class FeaturePageComponent {
private readonly injector = inject(Injector);
private readonly onboarding = inject(OnboardingService);

startFeatureTour(): void {
this.onboarding.start("feature-tour", {
injector: this.injector,
flowId: "feature-tour:default",
});
}
}

Guards are different: the engine calls a guard as a plain function when an interaction happens. If a guard needs a service, resolve that service while a context exists and close over it:

import { Injector, inject } from "@angular/core";
import { OnboardingService } from "@tsm/framework/onboarding";

export class FeaturePageComponent {
private readonly injector = inject(Injector);
private readonly onboarding = inject(OnboardingService);
private readonly permissions = inject(PermissionsService);

startFeatureTour(): void {
this.onboarding.start("feature-tour", {
injector: this.injector,
guards: {
canContinue: () => this.permissions.can("feature.use"),
},
});
}
}

The scenario can then reference the guard by name:

{
id: 'welcome',
overlay: {
content: {
kind: 'text',
text: 'Welcome.',
},
},
interactions: [
{
selector: '[data-cy="continue"]',
eventType: 'click',
condition: {
guard: 'canContinue',
},
action: {kind: 'next'},
},
],
}

Registering Scenarios

Scenarios can be registered globally through provideOnboarding({scenarios}). In TSM feature libraries and lazy plugins, prefer the multi-provider token so a plugin can contribute scenarios without central wiring.

import { Plugin } from "@tsm/framework/plugin";
import {
OnboardingScenarioRegistration,
TSM_ONBOARDING_SCENARIO_REGISTRATIONS,
} from "@tsm/framework/onboarding";

const onboardingScenarios: OnboardingScenarioRegistration[] = [
{
id: "hello-layout-tour",
scenario: {
id: "hello-layout-tour",
flowId: "default",
version: 1,
entryStepId: "welcome",
steps: [
{
id: "welcome",
target: {
selector: '[data-cy="onboarding-demo-panel"]',
paddingPx: 8,
},
overlay: {
content: {
kind: "text",
text: "This panel starts the onboarding demo.",
},
},
},
],
},
},
];

export const examplePlugin: Plugin = {
providers: [
{
provide: TSM_ONBOARDING_SCENARIO_REGISTRATIONS,
multi: true,
useValue: onboardingScenarios,
},
],
};

Starting a Tour

Inject OnboardingService from the component or service that owns the start action.

import { inject } from "@angular/core";
import {
InMemoryOnboardingProgressAdapter,
OnboardingService,
} from "@tsm/framework/onboarding";

const SCENARIO_ID = "hello-layout-tour";
const FLOW_ID = "hello-layout-tour:v1";

export class HelloComponent {
private readonly onboarding = inject(OnboardingService);

startOnboardingDemo(): void {
this.onboarding.start(SCENARIO_ID, {
flowId: FLOW_ID,
adapter: new InMemoryOnboardingProgressAdapter(),
});
}
}

The in-memory adapter is useful for demos and tests. In production, omit adapter unless the scenario needs special persistence. The default LocalStorageOnboardingProgressAdapter will be used.

Example: Layout Tour

The example below is a shortened version of the hello-world demo. It shows the main patterns:

  • CDK-style connected positions for precise overlay placement,
  • injected services inside a scenario factory,
  • a custom overlay component,
  • steps that wait for real clicks,
  • a final step that waits for an input event with a specific value.
import { DOCUMENT } from "@angular/common";
import { inject } from "@angular/core";
import { Router } from "@angular/router";
import { TranslocoService } from "@jsverse/transloco";
import { LayoutTsmMenuService } from "@tsm/layout/service";
import {
OnboardingConnectedPosition,
OnboardingScenarioRegistration,
} from "@tsm/framework/onboarding";

const gap = 16;

const belowTargetPositions: OnboardingConnectedPosition[] = [
{
originX: "center",
originY: "bottom",
overlayX: "center",
overlayY: "top",
offsetY: gap,
},
{
originX: "center",
originY: "top",
overlayX: "center",
overlayY: "bottom",
offsetY: -gap,
},
];

export const helloWorldTour: OnboardingScenarioRegistration = {
id: "hello-layout-tour",
guards: {
hasAhojInputValue: ({ event }) => {
const target = event?.target;
return target instanceof HTMLInputElement && target.value === "ahoj";
},
},
scenario: () => {
const document = inject(DOCUMENT);
const router = inject(Router);
const transloco = inject(TranslocoService);
const menu = inject(LayoutTsmMenuService);

const text = (key: string) => ({
kind: "text" as const,
text: transloco.translate(`helloWorld.onboarding.${key}`),
});

const waitForSelector = (selector: string): Promise<void> =>
new Promise((resolve) => {
const check = () => {
if (document.querySelector(selector)) {
resolve();
return;
}

requestAnimationFrame(check);
};

requestAnimationFrame(check);
});

return {
id: "hello-layout-tour",
flowId: "hello-layout-tour:v1",
version: 1,
entryStepId: "layout-overview",
componentRegistry: {
layoutOverview: {
loadComponent: () =>
import("@tsm/example/hello-world").then(
(module) => module.HelloOnboardingLayoutOverviewComponent,
),
},
},
steps: [
{
id: "layout-overview",
target: {
selector: '[data-cy="app-layout"]',
paddingPx: 8,
},
overlay: {
content: {
kind: "component",
componentKey: "layoutOverview",
},
showPrevious: false,
showNext: false,
},
},
{
id: "left-menu",
preActions: [() => menu.openMenu()],
target: {
selector: '[data-cy="app-left-menu"]',
paddingPx: 8,
},
overlay: {
positions: belowTargetPositions,
content: text("leftMenuText"),
},
},
{
id: "collapse-left-menu",
target: {
selector: '[data-cy="body-button-open-side-bar-menu"]',
paddingPx: 8,
},
overlay: {
positions: belowTargetPositions,
content: text("collapseLeftMenuText"),
showNext: false,
},
interactions: [
{
selector: '[data-cy="body-button-open-side-bar-menu"]',
eventType: "click",
action: { kind: "next" },
},
],
},
{
id: "return-to-hello",
preActions: [
() =>
router
.navigate(["/devel/hello"])
.then(() =>
waitForSelector('[data-cy="onboarding-demo-panel"]'),
),
],
target: {
selector: '[data-cy="onboarding-demo-panel"]',
paddingPx: 8,
},
overlay: {
positions: belowTargetPositions,
content: text("returnToHelloText"),
},
},
{
id: "type-ahoj",
target: {
selector: '[data-cy="onboarding-demo-ahoj-input"]',
paddingPx: 8,
},
overlay: {
positions: belowTargetPositions,
content: text("typeAhojText"),
showFinish: false,
showNext: false,
},
interactions: [
{
selector: '[data-cy="onboarding-demo-ahoj-input"]',
eventType: "input",
condition: {
guard: "hasAhojInputValue",
},
action: { kind: "next" },
},
],
},
],
};
},
};

The type-ahoj step has no visible Next or Finish button. It completes only after the highlighted input emits a real input event and the registered guard returns true.

Custom Step Component

Use kind: 'component' when a step needs real UI instead of plain text. The default host renders the component through NgComponentOutlet and passes a single context input.

The context contains navigation helpers and step-data helpers:

MethodUse
next()Move to the next step.
previous()Move to the previous step.
close()Abort the flow.
restart()Restart from entryStepId.
resume()Resume current or saved progress.
goTo(stepId)Jump to a specific step.
setStepData(key, value)Store data for the current step and persist progress.
getStepData(key)Read data stored for the current step.
nextWithData(stepData)Merge data into the current step and then move next.

Example component:

profile-menu-onboarding-step.component.ts
import {
ChangeDetectionStrategy,
Component,
computed,
input,
signal,
} from "@angular/core";
import {
OnboardingTemplateContext,
PreventOverlayCloseDirective,
} from "@tsm/framework/onboarding";
import { ButtonModule } from "primeng/button";
import { InputTextModule } from "primeng/inputtext";

@Component({
selector: "tsm-profile-menu-onboarding-step",
imports: [ButtonModule, InputTextModule, PreventOverlayCloseDirective],
templateUrl: "./profile-menu-onboarding-step.component.html",
styleUrl: "./profile-menu-onboarding-step.component.scss",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProfileMenuOnboardingStepComponent {
readonly context = input.required<OnboardingTemplateContext>();
readonly note = signal("");
readonly title = computed(() => this.context().step?.title ?? "Profile menu");

onNoteInput(event: Event): void {
const target = event.target;

if (!(target instanceof HTMLInputElement)) {
return;
}

this.note.set(target.value);
this.context().setStepData("profileMenuNote", target.value);
}

continue(): void {
this.context().nextWithData({
profileMenuConfirmed: true,
profileMenuNote: this.note(),
});
}

skipToInputStep(): void {
this.context().goTo("type-ahoj");
}

close(): void {
this.context().close();
}
}
profile-menu-onboarding-step.component.html
<section class="profile-menu-onboarding-step" tsmPreventOverlayClose>
<h4>{{ title() }}</h4>
<p>
This custom component can store data, jump between steps, close the flow, or
advance the flow without using the default Next button.
</p>

<input
pInputText
data-cy="profile-menu-onboarding-note"
type="text"
[value]="note()"
placeholder="Optional note"
(input)="onNoteInput($event)"
/>

<div class="profile-menu-onboarding-step__actions">
<p-button
type="button"
severity="secondary"
label="Skip"
(onClick)="skipToInputStep()"
/>
<p-button
type="button"
severity="secondary"
label="Close"
(onClick)="close()"
/>
<p-button type="button" label="Continue" (onClick)="continue()" />
</div>
</section>

Register the component under a key and reference that key from the step:

export const helloWorldTour: OnboardingScenarioRegistration = {
id: "hello-layout-tour",
scenario: {
id: "hello-layout-tour",
flowId: "hello-layout-tour:v1",
version: 1,
entryStepId: "profile-menu",
componentRegistry: {
profileMenuStep: {
loadComponent: () =>
import("./profile-menu-onboarding-step.component").then(
(module) => module.ProfileMenuOnboardingStepComponent,
),
},
},
steps: [
{
id: "profile-menu",
title: "Profile menu",
target: {
selector: '[data-cy="profile-menu"]',
paddingPx: 8,
},
overlay: {
content: {
kind: "component",
componentKey: "profileMenuStep",
},
showNext: false,
showFinish: false,
},
},
],
},
};

The important detail is showNext: false: the custom component owns the step transition by calling context().next(), context().nextWithData(...), or context().goTo(...).

Custom Host

Most flows should use the default host. A custom host is useful only when an application needs a different shell around the onboarding engine, custom telemetry, or a different overlay implementation.

A custom host is registered through provideOnboarding({hostComponent}).

import { provideOnboarding } from "@tsm/framework/onboarding";

export const appProviders = [
provideOnboarding({
hostComponent: {
loadComponent: () =>
import("./brand-onboarding-host.component").then(
(module) => module.BrandOnboardingHostComponent,
),
},
}),
];

The host outlet sets these inputs on the host component:

  • scenario,
  • flowId,
  • adapter,
  • guards,
  • templateRegistry,
  • componentRegistry,
  • actionInjector.

The host component should emit:

  • stateChange with OnboardingRuntimeState,
  • lifecycleEvent with OnboardingLifecycleEvent.

The simplest custom host wraps the default OnboardingHostComponent and adds a project-specific container:

brand-onboarding-host.component.ts
import {
ChangeDetectionStrategy,
Component,
Injector,
input,
output,
} from "@angular/core";
import {
OnboardingComponentRegistry,
OnboardingConditionPredicate,
OnboardingHostComponent,
OnboardingLifecycleEvent,
OnboardingProgressAdapter,
OnboardingRuntimeState,
OnboardingScenario,
OnboardingTemplateRegistry,
PreventOverlayCloseDirective,
} from "@tsm/framework/onboarding";

@Component({
selector: "tsm-brand-onboarding-host",
imports: [OnboardingHostComponent, PreventOverlayCloseDirective],
templateUrl: "./brand-onboarding-host.component.html",
styleUrl: "./brand-onboarding-host.component.scss",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BrandOnboardingHostComponent {
readonly scenario = input.required<OnboardingScenario>();
readonly flowId = input("");
readonly adapter = input<OnboardingProgressAdapter | undefined>(undefined);
readonly guards = input<Record<string, OnboardingConditionPredicate>>({});
readonly templateRegistry = input<OnboardingTemplateRegistry>({});
readonly componentRegistry = input<OnboardingComponentRegistry>({});
readonly actionInjector = input<Injector | null>(null);

readonly stateChange = output<OnboardingRuntimeState>();
readonly lifecycleEvent = output<OnboardingLifecycleEvent>();

onStateChange(state: OnboardingRuntimeState): void {
this.stateChange.emit(state);
}

onLifecycleEvent(event: OnboardingLifecycleEvent): void {
this.lifecycleEvent.emit(event);
}
}
brand-onboarding-host.component.html
<div class="brand-onboarding-host" tsmPreventOverlayClose>
<tsm-onboarding-host
[scenario]="scenario()"
[flowId]="flowId()"
[adapter]="adapter()"
[guards]="guards()"
[templateRegistry]="templateRegistry()"
[componentRegistry]="componentRegistry()"
[actionInjector]="actionInjector()"
(stateChange)="onStateChange($event)"
(lifecycleEvent)="onLifecycleEvent($event)"
/>
</div>

This wrapper still uses the standard engine, document interaction handling, content resolution, and injection-context behavior. A fully custom host can replace those pieces, but then it must preserve the same input/output contract so OnboardingRootHostComponent and OnboardingService can coordinate the run.

Scenario Options

provideOnboarding(options)

OptionTypeDescription
hostComponentOnboardingComponentRegistrationOverrides the default OnboardingHostComponent. Use only when a project needs a custom onboarding host implementation.
progressKeyBuilder(context) => string | Promise<string>Builds the storage key for the default local-storage adapter. Runs in an Angular injection context, so it can use inject().
scenariosOnboardingScenarioRegistration[]Registers scenarios during bootstrap. Plugin-based registration through TSM_ONBOARDING_SCENARIO_REGISTRATIONS is usually better for lazy feature libraries.

OnboardingService.start(scenarioId, options)

OptionTypeDescription
adapterOnboardingProgressAdapterPer-run progress adapter. Overrides the scenario adapter and the default local-storage adapter.
componentRegistryOnboardingComponentRegistryAdditional custom components for kind: 'component' overlay content. Merged after scenario and registration registries.
flowIdstringRuntime flow key. Use it to separate repeated runs of the same scenario, for example per entity, per user task, or per feature. Defaults to scenario.flowId or scenario.id.
guardsRecord<string, OnboardingConditionPredicate>Per-run condition predicates. Merged after registered guards.
injectorInjectorInjector used to resolve scenario factories and step actions. Useful when a scenario is started from a scoped feature injector.
scenarioOnboardingScenarioInline scenario used instead of a registered scenario. Useful for tests or one-off dynamic flows.
templateRegistryOnboardingTemplateRegistryAdditional Angular templates for kind: 'template' overlay content. Merged after scenario and registration registries.

OnboardingScenarioRegistration

OptionTypeDescription
idstringRegistration id used by OnboardingService.start(id).
scenarioOnboardingScenario | OnboardingScenarioFactoryStatic scenario object or a factory. Factories run in injection context and may call inject().
componentRegistryOnboardingComponentRegistryComponents available to all runs of this registration.
templateRegistryOnboardingTemplateRegistryTemplates available to all runs of this registration.
guardsRecord<string, OnboardingConditionPredicate>Named predicates used by step interaction conditions.
adapterOnboardingProgressAdapterScenario-specific progress adapter. Overridden by start(..., {adapter}).

OnboardingScenario

OptionTypeDescription
idstringStable scenario id.
flowIdstringOptional default flow id. Can be overridden when starting the scenario.
versionnumberPositive scenario version. Increment when step ids or progress semantics change. Saved progress is restored only when compatible with this version.
entryStepIdstringFirst step id. Must exist in steps.
stepsOnboardingScenarioStep[]Ordered list of steps.
componentRegistryOnboardingComponentRegistryScenario-local component registry.
metadataRecord<string, unknown>Free-form scenario metadata for custom hosts or analytics.

OnboardingScenarioStep

OptionTypeDescription
idstringStable step id. Required and unique within the scenario.
titlestringTitle shown in the overlay header.
subtitlestringSubtitle shown below the title.
descriptionstringFree-form description for custom consumers. The default overlay does not render it.
targetOnboardingTargetDescriptorElement to highlight and position against.
overlayOnboardingStepOverlayOverlay content, placement, and button visibility. Required.
interactionsOnboardingInteraction[]DOM interactions that can execute actions.
preActionsOnboardingStepAction[]Actions run before entering the step. They run in Angular injection context and may return a promise.
postActionsOnboardingStepAction[]Actions run when leaving the step. They run in Angular injection context and may return a promise.
nextStepIdstringExplicit next step id. Without it, the next item in steps is used.
metadataRecord<string, unknown>Free-form step metadata.

target

OptionTypeDescription
selectorstringCSS selector for the highlighted element. Use stable selectors such as data-cy.
paddingPxnumberHighlight padding. The root host currently clamps this to at least 4px; default is 10px.
fallbackToCenterbooleanReserved in the type layer. The current root host already centers the overlay if no target rect exists.
highlightStrategy'clip-path' | 'outline'Reserved in the type layer. The current root host uses the built-in outline/highlight style.
waitForMsnumberReserved in the type layer. For now use a preActions helper such as waitForSelector(...) when the target is rendered asynchronously.
scrollBehavior'always' | 'ifNeeded'Reserved in the type layer. The current host does not perform automatic scrolling.

overlay

OptionTypeDescription
position'center' | 'start' | 'end' | 'before' | 'after' | 'auto'Simple fallback placement. Used when positions is not provided or no connected position fits.
positionsOnboardingConnectedPosition[]Preferred placement list compatible with Angular CDK connected-position concepts. The first fitting position wins; otherwise the first position is clamped into the viewport.
titlestringReserved for custom hosts. The default overlay reads the step title.
subtitlestringReserved for custom hosts. The default overlay reads the step subtitle.
contentOnboardingOverlayContentDescriptorRequired overlay body content.
allowClosebooleanSet to false to disable close behavior.
showClosebooleanSet to false to hide the close button.
showFinishbooleanSet to false to hide Finish on the terminal step.
showPreviousbooleanSet to false to hide Previous.
showNextbooleanSet to false to hide Next. Use this for steps that wait for a real interaction.

content

KindOptionsDescription
text{kind: 'text', text}Plain text rendered in the overlay body.
markdown{kind: 'markdown', text}Currently rendered like plain text by the default overlay. Use html, template, or component for rich content.
html{kind: 'html', html}Sanitized HTML rendered through Angular DomSanitizer.
template{kind: 'template', templateKey}Renders a template from templateRegistry with OnboardingTemplateContext.
component{kind: 'component', componentKey}Renders a component from componentRegistry. The component receives context input.

OnboardingConnectedPosition

OptionTypeDescription
originX'start' | 'center' | 'end'Horizontal anchor point on the target.
originY'top' | 'center' | 'bottom'Vertical anchor point on the target.
overlayX'start' | 'center' | 'end'Horizontal anchor point on the overlay.
overlayY'top' | 'center' | 'bottom'Vertical anchor point on the overlay.
offsetXnumberHorizontal offset in pixels.
offsetYnumberVertical offset in pixels.
panelClassstring | string[]Reserved for custom host implementations. The current root host does not apply it.

interactions

OptionTypeDescription
selectorstringCSS selector that must match the event target or one of its ancestors.
eventType'click' | 'focus' | 'input' | 'hover' | 'manual'Event to listen for. The default host maps document click, focusin, input, and mouseover to these values. manual is available for custom hosts or direct engine dispatching.
actionOnboardingActionAction executed when the selector and condition match.
conditionOnboardingConditionOptional guard and step-data requirement.
preventDefaultbooleanCalls event.preventDefault() before dispatching the action.

condition

OptionTypeDescription
requiredStepDatastring[]Requires keys to be present in current step data before the interaction can run.
guardstringName of a registered predicate. Guards receive {condition, state, step, event}.

action

KindRequired fieldsBehavior
next-Move to the next step or complete the scenario on the terminal step.
previous-Move to the previous step.
close-Abort the flow. The root host then removes the active run.
jumpstepIdJump to a specific step.
restart-Reset state and enter the scenario's entryStepId.
resume-Resume saved or current progress.
customeventName, payload optionalEmits a lifecycle event with metadata. It does not change steps by itself.
pause-Reserved for future support. Current engine emits a blocked event.
start-Reserved in the action type. Start flows through OnboardingService.start(...), not through scenario interactions.

Progress Adapters

The progress adapter interface is:

export interface OnboardingProgressAdapter {
loadProgress(
request,
): OnboardingRuntimeState | null | Promise<OnboardingRuntimeState | null>;
saveProgress(snapshot): void | Promise<void>;
deleteProgress(request): void | Promise<void>;
}

Built-in adapters:

AdapterUse
LocalStorageOnboardingProgressAdapterDefault adapter. Stores snapshots in browser localStorage. Uses progressKeyBuilder.
NullOnboardingProgressAdapterDoes not load, save, or delete progress. Use when progress must never persist.
InMemoryOnboardingProgressAdapterKeeps progress in memory. Useful for demos and tests.

Progress state stores:

  • scenarioId,
  • scenarioVersion,
  • flowId,
  • current status,
  • current step id/index,
  • completed step ids,
  • per-step data,
  • transition history,
  • update timestamp.

Step Actions and Injection

Scenario factories and step actions run in an Angular injection context.

{
id: 'open-profile-detail',
preActions: [
() => {
const router = inject(Router);
return router.navigate(['/user-management/user/current']);
},
],
target: {
selector: '[data-cy="user-detail-tabs"]',
},
overlay: {
content: {
kind: 'text',
text: 'The user detail is organized into tabs.',
},
},
}

Use preActions for work that must happen before the step is displayed: opening a menu, waiting for a selector, navigating to a route, or closing a temporary overlay. Use postActions for cleanup after leaving a step.

Working With Application Overlays

The root host card uses tsmPreventOverlayClose, which stops click, mouse, pointer, and touch events from bubbling out of the onboarding card. This is important when a step is explaining an application overlay such as the profile menu. Clicking Next in the onboarding card should not close the application overlay unless the scenario explicitly asks for that.

If you create a custom onboarding host or custom content that should preserve an open application overlay, apply the directive to the container that receives the click.

<div tsmPreventOverlayClose>
<!-- custom onboarding controls -->
</div>

Styling

The default root host exposes TSM CSS variables and falls back to Prime/TSM theme tokens:

VariableDefault
--tsm-onboarding-primaryvar(--p-primary-color, #2563eb)
--tsm-onboarding-textvar(--p-text-color, #111827)
--tsm-onboarding-mutedvar(--p-text-muted-color, #64748b)
--tsm-onboarding-surfacevar(--p-content-background, #fff)
--tsm-onboarding-bordervar(--p-content-border-color, rgb(17 19 24 / 0.1))
--onboarding-root-host-panel-max-widthmin(26rem, calc(100vw - 2rem))

Override these variables at application or theme level to tune the onboarding appearance.

Validation

Use validateOnboardingScenario in tests or development utilities to catch common mistakes before a scenario is registered.

import { validateOnboardingScenario } from "@tsm/framework/onboarding";

const result = validateOnboardingScenario(scenario);

if (!result.ok) {
console.table(result.issues);
}

The validator checks required scenario fields, positive versions, duplicate step ids, missing entry steps, empty content, missing nextStepId targets, invalid interaction events, invalid interaction actions, missing selectors, and jump actions without stepId.

  • Use stable data-cy selectors for targets and interactions.
  • Put the root host in the application layout, not in a routed page.
  • Use positions for real tours. Avoid position: 'center' for targeted steps unless a centered overlay is intentional.
  • Hide Next/Finish for steps that must wait for a real user event.
  • Keep scenario text in the normal Transloco/i18n structure of the owning feature or plugin.
  • Increment scenario.version when you rename step ids or change progress semantics.
  • Use flowId to separate multiple independent runs of the same scenario.
  • Prefer plugin registration for scenarios owned by lazy feature libraries.