Wizard
A full-screen step-by-step flow for onboarding and setup: the step with its progress, heading, content and footer on the left, an illustrated stage that slides its scenes on the right.
A block from @tectra-ng/blocks — it comes with tectra-ng, or alone: npm install @tectra-ng/blocks. The examples sit in a 560px screen; in an app the wizard fills the window.
Basic
Four steps with a form, an optional step and a finish with a loading button. The stage on the right slides its scenes forward or back and shows the step number and name; the steps rise in with tx-wizard-step--enter.
Welcome to Acme
Three short steps and your workspace is ready.
<div class="screen"> <tx-wizard-layout [dt]="{ layout: { minHeight: '558px' } }"> <div txWizardHeader class="brand-row"> <span class="brand"><img src="/favicon.svg" alt="" width="24" height="24" /> Acme</span> <tx-wizard-progress [total]="steps.length" [current]="index()" [label]="'Step ' + (index() + 1) + ' of ' + steps.length" [stepLabels]="titles" /> </div> <div txWizardBody> @switch (step().id) { @case ('welcome') { <section class="step" animate.enter="tx-wizard-step--enter"> <tx-wizard-step-head icon="sparkles" title="Welcome to Acme" lead="Three short steps and your workspace is ready." /> </section> } @case ('profile') { <section class="step" animate.enter="tx-wizard-step--enter"> <tx-wizard-step-head icon="user" title="Your profile" lead="Tell the team who you are." /> <div class="fields"> <div> <tx-label for="wizard-name">Full name</tx-label> <input txInput id="wizard-name" fluid [(ngModel)]="name" /> </div> <div> <tx-label for="wizard-role">Role</tx-label> <tx-select inputId="wizard-role" [options]="roles" [(ngModel)]="role" /> </div> </div> </section> } @case ('team') { <section class="step" animate.enter="tx-wizard-step--enter"> <tx-wizard-step-head icon="users" title="Invite your team" badge="Optional" lead="Pick who joins now; you can invite more later." /> <div class="fields"> @for (person of people; track person) { <tx-checkbox binary [label]="person" [ngModel]="invited().includes(person)" (ngModelChange)="toggle(person)" /> } </div> </section> } @default { <section class="step" animate.enter="tx-wizard-step--enter"> <tx-wizard-step-head icon="check-circle" title="All set" [lead]="'Your workspace is ready, ' + (firstName() || 'friend') + '.'" /> </section> } } </div> <tx-wizard-footer txWizardFooter [showBack]="index() > 0" [skipLabel]="step().id === 'team' ? 'Skip' : undefined" [nextLabel]="isLast() ? 'Finish' : 'Next'" [nextIcon]="isLast() ? 'check' : 'arrow-right'" [loading]="loading()" (back)="go(-1)" (next)="isLast() ? finish() : go(1)" (skip)="go(1)" /> <tx-wizard-stage txWizardVisual [direction]="direction()" [number]="index() + 1" [caption]="step().title" > <div txWizardBackdrop class="dots"></div> @switch (step().id) { @case ('welcome') { <div class="tx-wizard-scene" animate.enter="tx-wizard-scene--enter" animate.leave="tx-wizard-scene--leave" > <div class="scene"> <div class="tile"><tx-icon name="sparkles" /></div> </div> </div> } @case ('profile') { <div class="tx-wizard-scene" animate.enter="tx-wizard-scene--enter" animate.leave="tx-wizard-scene--leave" > <div class="scene"> <div class="card"> <tx-avatar size="xlarge" [name]="name() || 'You'" /> <strong>{{ name() || 'Your name' }}</strong> <tx-pill tone="info" size="small" [value]="roleLabel()" /> </div> </div> </div> } @case ('team') { <div class="tx-wizard-scene" animate.enter="tx-wizard-scene--enter" animate.leave="tx-wizard-scene--leave" > <div class="scene"> <div class="card"> <div class="avatars"> @for (person of invited(); track person) { <tx-avatar size="large" [name]="person" /> } @empty { <tx-avatar size="large" icon="users" /> } </div> <span>{{ invited().length }} invited</span> </div> </div> </div> } @default { <div class="tx-wizard-scene" animate.enter="tx-wizard-scene--enter" animate.leave="tx-wizard-scene--leave" > <div class="scene"> <div class="tile tile--done"><tx-icon name="check" /></div> </div> </div> } } </tx-wizard-stage> </tx-wizard-layout></div>Responsive
Below 1024px the stage becomes a band above the step and shows a smaller scene; from 640px the step gets wider side spacing. responsive="container" measures the wizard itself — pick a device to see each width.
Welcome to Acme
Three short steps and your workspace is ready.
<tx-tabs class="devices" variant="pills" ariaLabel="Device" [tabs]="devices" [(value)]="device"/><div class="stage"> <div class="screen" [style.width]="width()"> <tx-wizard-layout responsive="container" [dt]="{ layout: { minHeight: '558px' } }"> <div txWizardHeader class="brand-row"> <span class="brand"><img src="/favicon.svg" alt="" width="24" height="24" /> Acme</span> <tx-wizard-progress [total]="steps.length" [current]="index()" [label]="'Step ' + (index() + 1) + ' of ' + steps.length" [stepLabels]="titles" /> </div> <div txWizardBody> @switch (step().id) { @case ('welcome') { <section class="step" animate.enter="tx-wizard-step--enter"> <tx-wizard-step-head icon="sparkles" title="Welcome to Acme" lead="Three short steps and your workspace is ready." /> </section> } @case ('profile') { <section class="step" animate.enter="tx-wizard-step--enter"> <tx-wizard-step-head icon="user" title="Your profile" lead="Tell the team who you are." /> <div class="fields"> <div> <tx-label for="wizard-name">Full name</tx-label> <input txInput id="wizard-name" fluid [(ngModel)]="name" /> </div> <div> <tx-label for="wizard-role">Role</tx-label> <tx-select inputId="wizard-role" [options]="roles" [(ngModel)]="role" /> </div> </div> </section> } @case ('team') { <section class="step" animate.enter="tx-wizard-step--enter"> <tx-wizard-step-head icon="users" title="Invite your team" badge="Optional" lead="Pick who joins now; you can invite more later." /> <div class="fields"> @for (person of people; track person) { <tx-checkbox binary [label]="person" [ngModel]="invited().includes(person)" (ngModelChange)="toggle(person)" /> } </div> </section> } @default { <section class="step" animate.enter="tx-wizard-step--enter"> <tx-wizard-step-head icon="check-circle" title="All set" [lead]="'Your workspace is ready, ' + (firstName() || 'friend') + '.'" /> </section> } } </div> <tx-wizard-footer txWizardFooter [showBack]="index() > 0" [skipLabel]="step().id === 'team' ? 'Skip' : undefined" [nextLabel]="isLast() ? 'Finish' : 'Next'" [nextIcon]="isLast() ? 'check' : 'arrow-right'" [loading]="loading()" (back)="go(-1)" (next)="isLast() ? finish() : go(1)" (skip)="go(1)" /> <tx-wizard-stage txWizardVisual [direction]="direction()" [number]="index() + 1" [caption]="step().title" > <div txWizardBackdrop class="dots"></div> @switch (step().id) { @case ('welcome') { <div class="tx-wizard-scene" animate.enter="tx-wizard-scene--enter" animate.leave="tx-wizard-scene--leave" > <div class="scene"> <div class="tile"><tx-icon name="sparkles" /></div> </div> </div> } @case ('profile') { <div class="tx-wizard-scene" animate.enter="tx-wizard-scene--enter" animate.leave="tx-wizard-scene--leave" > <div class="scene"> <div class="card"> <tx-avatar size="xlarge" [name]="name() || 'You'" /> <strong>{{ name() || 'Your name' }}</strong> <tx-pill tone="info" size="small" [value]="roleLabel()" /> </div> </div> </div> } @case ('team') { <div class="tx-wizard-scene" animate.enter="tx-wizard-scene--enter" animate.leave="tx-wizard-scene--leave" > <div class="scene"> <div class="card"> <div class="avatars"> @for (person of invited(); track person) { <tx-avatar size="large" [name]="person" /> } @empty { <tx-avatar size="large" icon="users" /> } </div> <span>{{ invited().length }} invited</span> </div> </div> </div> } @default { <div class="tx-wizard-scene" animate.enter="tx-wizard-scene--enter" animate.leave="tx-wizard-scene--leave" > <div class="scene"> <div class="tile tile--done"><tx-icon name="check" /></div> </div> </div> } } </tx-wizard-stage> </tx-wizard-layout> </div></div>Parts
The parts work on their own too: the progress for any multi-step form, the step head as the heading of a setup page, the footer with its states — a hidden back button on the first step, a skip for optional steps and a loading finish.
Company details
We use them on invoices and in the emails your customers get.
<div class="column"> <tx-wizard-progress [total]="5" [current]="current()" [label]="'Step ' + (current() + 1) + ' of 5'" /> <tx-wizard-step-head icon="building" title="Company details" badge="Optional" lead="We use them on invoices and in the emails your customers get." /> <tx-wizard-footer [showBack]="current() > 0" [skipLabel]="current() === 2 ? 'Skip' : undefined" [nextLabel]="current() === 4 ? 'Finish' : 'Next'" [nextIcon]="current() === 4 ? 'check' : 'arrow-right'" [loading]="loading()" (back)="current.set(current() - 1)" (skip)="current.set(current() + 1)" (next)="next()" /></div>Design tokens
dt changes the tokens of one wizard part: here thicker, rounder progress segments and a larger step icon.
Launch
Everything is checked. Start when you are ready.
<div class="column"> <tx-wizard-progress label="Step 3 of 4" [total]="4" [current]="2" [dt]="{ progress: { segmentHeight: '8px', segmentGap: '8px', currentOpacity: '0.4' } }" /> <tx-wizard-step-head icon="send" title="Launch" lead="Everything is checked. Start when you are ready." [dt]="{ stepHead: { iconSize: '48px', iconFontSize: '20px', iconBorderRadius: '24px' } }" /></div>