Onboarding Wizard
blockauthMulti-step workspace setup flow in a SectionCard: a four-step Stepper (Workspace → Team → Preferences → Done) with check icons on completed steps, a workspace-name input with live URL slug preview, teammate invites as removable email chips with a skip link, preference Switch rows, and a success panel with a glowing check circle. Footer carries Back/Continue actions plus a Step X of 4 counter. Data is stubbed inline — wire your API of choice.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/onboarding-wizard.json$npx shadcn@latest add https://uipkge.dev/r/lit/onboarding-wizard.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/onboarding-wizard.json$bunx shadcn@latest add https://uipkge.dev/r/lit/onboarding-wizard.jsonManifest:
https://uipkge.dev/r/lit/onboarding-wizard.jsonInstalls to:src/uip/blocks/onboarding-wizard/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
initial-step | number | 1 | optional |
initial-workspace-name | string | Acme Inc | optional |
initial-invites | string | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
WizardPreferenceinterface WizardPreference {
id: string
label: string
description: string
enabled: boolean
}HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-onboarding-wizard': UipOnboardingWizard
}
}Files installed (1)
src/uip/blocks/onboarding-wizard/onboarding-wizard.ts11.6 kB
import { LitElement, css, html, nothing } from 'lit' import { Check, ChevronLeft, ChevronRight, Globe, Plus, X } from 'lucide' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' import { icon } from '../../lib/icon' // Side-effect imports: register the composed elements for standalone use // (consumers who install only this block still get working primitives). import '../../components/badge/badge' import '../../components/button/button' import '../../components/input/input' import '../../components/section-card/section-card' import '../../components/stepper/stepper' import '../../components/switch/switch' import type { UipInput } from '../../components/input/input' interface WizardPreference { id: string label: string description: string enabled: boolean } const STEPS = [{ title: 'Workspace' }, { title: 'Team' }, { title: 'Preferences' }, { title: 'Done' }] const DEFAULT_PREFERENCES: WizardPreference[] = [ { id: 'digests', label: 'Email digests', description: 'A daily summary of workspace activity.', enabled: true }, { id: 'updates', label: 'Product updates', description: 'Occasional notes about new features and improvements.', enabled: false, }, { id: 'reports', label: 'Weekly reports', description: 'Usage analytics delivered every Monday morning.', enabled: true, }, ] /** * <uip-onboarding-wizard> — the registry OnboardingWizard block as a web component. * * Class strings are React's OnboardingWizard verbatim. React state becomes * @state fields. `initial-step` (React `initialStep`) jumps straight to a * step; `initial-workspace-name` and `initial-invites` (comma-separated) * pre-fill state. React's `className` maps to classes on the host. The slug * preview, invite chips, and preference switches behave as in React. */ export class UipOnboardingWizard extends LitElement { static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true } // :host display can't be a utility class (the host has no template of its own). static styles = [tailwind, css`:host { display: block; }`] static properties = { initialStep: { type: Number, attribute: 'initial-step' }, initialWorkspaceName: { attribute: 'initial-workspace-name' }, initialInvites: { attribute: 'initial-invites' }, step: { state: true }, workspaceName: { state: true }, invites: { state: true }, inviteDraft: { state: true }, inviteInvalid: { state: true }, preferences: { state: true }, } initialStep = 1 initialWorkspaceName = 'Acme Inc' initialInvites = '' private step = 1 private workspaceName = 'Acme Inc' private invites: string[] = [] private inviteDraft = '' private inviteInvalid = false private preferences: WizardPreference[] = DEFAULT_PREFERENCES.map((p) => ({ ...p })) constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'onboarding-wizard') this.step = this.initialStep this.workspaceName = this.initialWorkspaceName this.invites = this.initialInvites ? this.initialInvites .split(',') .map((s) => s.trim()) .filter(Boolean) : [] } private get slug(): string { return ( this.workspaceName .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') || 'your-workspace' ) } private addInvite() { const email = this.inviteDraft.trim() if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || this.invites.includes(email)) { this.inviteInvalid = true return } this.inviteInvalid = false this.invites = [...this.invites, email] this.inviteDraft = '' } private removeInvite(email: string) { this.invites = this.invites.filter((e) => e !== email) } private onStepperInput(e: CustomEvent<{ value: number }>) { // Free backward navigation; forward goes through the footer button only. if (e.detail.value < this.step && this.step !== 4) this.step = e.detail.value } private goNext() { if (this.step < 4) this.step += 1 } private reset() { this.step = 1 this.workspaceName = '' this.invites = [] this.inviteDraft = '' } render() { return html` <uip-section-card data-slot="onboarding-wizard" heading="Set up your workspace" description="Four quick steps and your team is ready to collaborate." > <uip-stepper .steps=${STEPS} value=${this.step} class="mb-6" @value-change=${this.onStepperInput} ></uip-stepper> ${this.step === 1 ? html` <div class="space-y-4"> <div class="space-y-2"> <label for="onboarding-workspace-name" class="text-sm font-medium">Workspace name</label> <uip-input id="onboarding-workspace-name" .value=${this.workspaceName} placeholder="Acme Inc" autocomplete="off" @input=${(e: Event) => { this.workspaceName = (e.target as UipInput).value }} ></uip-input> </div> <div class="border-border bg-muted/40 flex items-center gap-2 rounded-md border px-3 py-2"> ${icon(Globe, 'globe', 'text-muted-foreground size-4 shrink-0')} <p class="min-w-0 truncate text-xs"> <span class="text-muted-foreground">uipkge.dev/</span> <span class="text-foreground font-medium">${this.slug}</span> </p> </div> </div> ` : nothing} ${this.step === 2 ? html` <div class="space-y-4"> <form class="flex gap-2" @submit=${(e: Event) => { e.preventDefault() this.addInvite() }} > <uip-input .value=${this.inviteDraft} type="email" placeholder="[email protected]" status=${this.inviteInvalid ? 'error' : nothing} class="flex-1" @input=${(e: Event) => { this.inviteDraft = (e.target as UipInput).value this.inviteInvalid = false }} ></uip-input> <uip-button type="submit" variant="outline" size="sm" class="shrink-0"> ${icon(Plus, 'plus')} Add </uip-button> </form> ${this.inviteInvalid ? html`<p role="alert" class="text-destructive text-xs">Enter a valid email address.</p>` : nothing} ${this.invites.length > 0 ? html`<div class="flex flex-wrap gap-2"> ${this.invites.map( (email) => html` <uip-badge variant="secondary" class="gap-1 py-1 pr-1 pl-2.5"> ${email} <button type="button" aria-label=${`Remove ${email}`} class="hover:bg-foreground/10 focus-visible:ring-ring min-h-6 rounded-full p-0.5 transition-colors focus-visible:ring-2 focus-visible:outline-none" @click=${() => this.removeInvite(email)} > ${icon(X, 'x', 'size-3')} </button> </uip-badge> `, )} </div>` : html`<p class="text-muted-foreground text-xs">No teammates invited yet.</p>`} <button type="button" class="text-muted-foreground hover:text-foreground focus-visible:ring-ring min-h-6 text-xs underline-offset-4 transition-colors hover:underline focus-visible:ring-2 focus-visible:outline-none" @click=${this.goNext} > Skip for now </button> </div> ` : nothing} ${this.step === 3 ? html` <div class="divide-y rounded-lg border"> ${this.preferences.map( (pref) => html` <div class="flex items-center justify-between gap-4 p-4"> <div class="min-w-0"> <p class="text-sm font-medium">${pref.label}</p> <p class="text-muted-foreground mt-0.5 text-xs">${pref.description}</p> </div> <uip-switch aria-label=${pref.label} .checked=${pref.enabled} @checked-change=${(e: CustomEvent<boolean>) => { const enabled = e.detail this.preferences = this.preferences.map((p) => p.id === pref.id ? { ...p, enabled } : p, ) }} ></uip-switch> </div> `, )} </div> ` : nothing} ${this.step === 4 ? html` <div class="space-y-4 py-6 text-center"> <div class="border-success/30 bg-success/10 text-success mx-auto flex size-16 items-center justify-center rounded-full border shadow-xs" > ${icon(Check, 'check', 'size-8')} </div> <div> <p class="text-lg font-semibold">You're all set</p> <p class="text-muted-foreground mx-auto mt-1 max-w-sm text-sm"> ${this.invites.length > 0 ? `${this.workspaceName || 'Your workspace'} is ready and ${this.invites.length} invitation${this.invites.length === 1 ? '' : 's'} sent.` : `${this.workspaceName || 'Your workspace'} is ready — invite teammates anytime from settings.`} </p> </div> <uip-button variant="outline" size="sm" @click=${this.reset}>Set up another workspace</uip-button> </div> ` : nothing} <div slot="footer" class="flex w-full items-center justify-between"> ${this.step > 1 && this.step < 4 ? html`<uip-button variant="ghost" size="sm" @click=${() => { this.step -= 1 }} > ${icon(ChevronLeft, 'chevron-left')} Back </uip-button>` : html`<span aria-hidden="true"></span>`} <span class="text-muted-foreground text-xs">Step ${this.step} of 4</span> ${this.step < 4 ? html`<uip-button size="sm" ?disabled=${this.step === 1 && this.workspaceName.trim() === ''} @click=${this.goNext} > ${this.step === 3 ? 'Finish' : 'Continue'} ${icon(ChevronRight, 'chevron-right')} </uip-button>` : html`<span aria-hidden="true"></span>`} </div> </uip-section-card> ` } } customElements.get('uip-onboarding-wizard') || customElements.define('uip-onboarding-wizard', UipOnboardingWizard) declare global { interface HTMLElementTagNameMap { 'uip-onboarding-wizard': UipOnboardingWizard } }
Raw manifest:https://uipkge.dev/r/lit/onboarding-wizard.json