Onboarding Wizard
A 4-step tenant onboarding flow: Organisation details → Plan selection → Team setup → Review & confirm.
Components used
Wizard · Stepper · WizardNav · Step · SummaryCard · Input · Select · RadioGroup · Button
Code
- HTML · @primus/ui-core
- React
- Angular
<div style="background:var(--muted);min-height:500px;display:flex;align-items:flex-start;justify-content:center;padding:2rem;box-sizing:border-box">
<div style="width:100%;max-width:640px">
<!-- Progress -->
<div style="margin-bottom:1.5rem">
<div style="display:flex;justify-content:space-between;margin-bottom:0.5rem;font-size:0.8rem">
<span style="font-weight:500">Step 2 of 4 — Plan Selection</span>
<span style="color:var(--muted-foreground)">50% complete</span>
</div>
<div style="height:4px;background:var(--border);border-radius:4px;overflow:hidden">
<div style="width:50%;height:100%;background:var(--primary);border-radius:4px"></div>
</div>
<div style="display:flex;gap:0;margin-top:0.75rem;border:1px solid var(--border);border-radius:6px;overflow:hidden;font-size:0.8rem">
<div style="flex:1;padding:0.5rem 0.75rem;background:color-mix(in srgb,var(--primary) 10%,transparent);color:var(--primary);text-align:center;font-weight:500">✓ Details</div>
<div style="flex:1;padding:0.5rem 0.75rem;background:var(--primary);color:var(--primary-foreground);text-align:center;font-weight:600;border-left:1px solid var(--border)">Plan</div>
<div style="flex:1;padding:0.5rem 0.75rem;text-align:center;color:var(--muted-foreground);border-left:1px solid var(--border)">Team</div>
<div style="flex:1;padding:0.5rem 0.75rem;text-align:center;color:var(--muted-foreground);border-left:1px solid var(--border)">Review</div>
</div>
</div>
<!-- Step card -->
<div class="card" style="padding:2rem">
<h2 style="margin:0 0 0.5rem;font-size:1.125rem;font-weight:700">Choose your plan</h2>
<p style="margin:0 0 1.5rem;font-size:0.875rem;color:var(--muted-foreground)">Select the plan that fits your team. You can upgrade at any time.</p>
<div class="vstack" style="gap:0.75rem">
<!-- Plan options as radio-style cards -->
<label style="display:flex;align-items:flex-start;gap:1rem;padding:1rem;border:1px solid var(--border);border-radius:8px;cursor:pointer">
<input type="radio" name="plan" style="margin-top:3px" />
<div>
<div style="font-weight:600;font-size:0.9rem">Starter</div>
<div style="font-size:0.8rem;color:var(--muted-foreground);margin:0.25rem 0">Up to 10 users · 5GB storage</div>
<div style="font-weight:700;font-size:1rem">$49 <span style="font-weight:400;font-size:0.8rem;color:var(--muted-foreground)">/month</span></div>
</div>
</label>
<label style="display:flex;align-items:flex-start;gap:1rem;padding:1rem;border:2px solid var(--primary);border-radius:8px;cursor:pointer;background:color-mix(in srgb,var(--primary) 4%,transparent)">
<input type="radio" name="plan" checked style="margin-top:3px" />
<div style="flex:1">
<div style="display:flex;align-items:center;gap:0.5rem">
<span style="font-weight:600;font-size:0.9rem">Pro</span>
<span class="badge accent" style="font-size:0.65rem">Recommended</span>
</div>
<div style="font-size:0.8rem;color:var(--muted-foreground);margin:0.25rem 0">Up to 100 users · 50GB storage</div>
<div style="font-weight:700;font-size:1rem">$249 <span style="font-weight:400;font-size:0.8rem;color:var(--muted-foreground)">/month</span></div>
</div>
</label>
<label style="display:flex;align-items:flex-start;gap:1rem;padding:1rem;border:1px solid var(--border);border-radius:8px;cursor:pointer">
<input type="radio" name="plan" style="margin-top:3px" />
<div>
<div style="font-weight:600;font-size:0.9rem">Enterprise</div>
<div style="font-size:0.8rem;color:var(--muted-foreground);margin:0.25rem 0">Unlimited users · custom storage</div>
<div style="font-weight:700;font-size:1rem">Custom pricing</div>
</div>
</label>
</div>
</div>
<!-- Nav -->
<div style="display:flex;justify-content:space-between;margin-top:1rem">
<button data-variant="secondary" class="small">← Back</button>
<button class="small">Next — Team Setup →</button>
</div>
</div>
</div>
import {
PrimusWizard, PrimusWizardStep, PrimusWizardNav,
PrimusStepper, PrimusSummaryCard,
PrimusInput, PrimusSelect, PrimusRadioGroup, PrimusButton,
} from 'primus-react-ui';
import { useState } from 'react';
const STEPS = ['Details', 'Plan', 'Team', 'Review'];
const planOptions = [
{ value: 'starter', label: 'Starter — $49/month' },
{ value: 'pro', label: 'Pro — $249/month' },
{ value: 'enterprise', label: 'Enterprise — Custom' },
];
export function OnboardingWizard() {
const [step, setStep] = useState(0);
const [form, setForm] = useState({ orgName: '', domain: '', plan: 'pro', seats: 10 });
const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
return (
<PrimusWizard
title="Set up your organisation"
activeStep={step}
onStepChange={setStep}
>
<PrimusStepper steps={STEPS} activeStep={step} completedSteps={Array.from({ length: step }, (_, i) => i)} />
<PrimusWizardStep label="Details" stepNumber={1}>
<PrimusInput label="Organisation name" required value={form.orgName}
onChange={v => set('orgName', v)} placeholder="Acme Corp" />
<PrimusInput label="Domain" value={form.domain}
onChange={v => set('domain', v)} placeholder="acme.com"
hint="Used for SSO and email verification" />
</PrimusWizardStep>
<PrimusWizardStep label="Plan" stepNumber={2}>
<PrimusRadioGroup name="plan" options={planOptions}
value={form.plan} onChange={v => set('plan', v)} />
</PrimusWizardStep>
<PrimusWizardStep label="Team" stepNumber={3}>
<PrimusInput label="Invite admin email" type="email"
placeholder="admin@acme.com" />
<PrimusInput label="Max seats" type="number"
value={form.seats} onChange={v => set('seats', Number(v))} />
</PrimusWizardStep>
<PrimusWizardStep label="Review" stepNumber={4}>
<PrimusSummaryCard title="Review your setup">
<p>Organisation: {form.orgName}</p>
<p>Plan: {form.plan}</p>
<p>Domain: {form.domain}</p>
</PrimusSummaryCard>
<PrimusButton style={{ width: '100%' }} onClick={handleComplete}>
Create organisation
</PrimusButton>
</PrimusWizardStep>
<PrimusWizardNav
onBack={() => setStep(s => Math.max(0, s - 1))}
onNext={() => setStep(s => Math.min(STEPS.length - 1, s + 1))}
nextLabel={step === STEPS.length - 1 ? 'Create organisation' : 'Next'}
/>
</PrimusWizard>
);
}
<!-- onboarding-wizard.component.html -->
<div class="wizard-shell">
<primus-stepper
[steps]="steps" [activeStep]="activeStep"
[completedSteps]="completedSteps">
</primus-stepper>
<primus-wizard [title]="stepTitle" [subtitle]="stepSubtitle">
<ng-container [ngSwitch]="activeStep">
<!-- Step 1: Organisation details -->
<ng-container *ngSwitchCase="0">
<primus-input label="Organisation name" [required]="true"
[(value)]="form.orgName" placeholder="Acme Corp">
</primus-input>
<primus-input label="Domain" [(value)]="form.domain"
placeholder="acme.com" hint="Used for SSO and email verification">
</primus-input>
</ng-container>
<!-- Step 2: Plan -->
<ng-container *ngSwitchCase="1">
<primus-radio-group name="plan"
[options]="planOptions" [(value)]="form.plan">
</primus-radio-group>
</ng-container>
<!-- Step 3: Team -->
<ng-container *ngSwitchCase="2">
<primus-input label="Invite admin email" type="email"
[(value)]="form.adminEmail">
</primus-input>
<primus-input label="Max seats" type="number"
[(value)]="form.seats">
</primus-input>
</ng-container>
<!-- Step 4: Review -->
<ng-container *ngSwitchCase="3">
<primus-summary-card title="Review your setup">
<div>Organisation: {{ form.orgName }}</div>
<div>Plan: {{ form.plan }}</div>
<div>Domain: {{ form.domain }}</div>
</primus-summary-card>
</ng-container>
</ng-container>
<primus-wizard-nav
[backLabel]="'Back'"
[nextLabel]="activeStep === 3 ? 'Create organisation' : 'Next'"
(back)="onBack()"
(next)="onNext()">
</primus-wizard-nav>
</primus-wizard>
</div>