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-svelte@latest add https://uipkge.dev/r/svelte/onboarding-wizard.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/onboarding-wizard.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/onboarding-wizard.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/onboarding-wizard.jsonManifest:
https://uipkge.dev/r/svelte/onboarding-wizard.jsonInstalls to:src/lib/components/blocks/onboarding-wizard/Variants
Loading interactive previews…
Loading interactive previews…
npm dependencies
Files installed (1)
src/lib/components/blocks/onboarding-wizard/OnboardingWizard.svelte8.3 kB
<script lang="ts"> import { Check, ChevronLeft, ChevronRight, Globe, Plus, X } from '@lucide/svelte' import { Badge } from '$lib/components/ui/badge' import { Button } from '$lib/components/ui/button' import { Input } from '$lib/components/ui/input' import { SectionCard } from '$lib/components/ui/section-card' import { Stepper, type StepperStepConfig } from '$lib/components/ui/stepper' import { Switch } from '$lib/components/ui/switch' interface Preference { id: string label: string description: string enabled: boolean } interface OnboardingWizardProps { /** Jump straight to a step (1 Workspace · 2 Team · 3 Preferences · 4 Done). */ initialStep?: number /** Pre-fill the workspace name so later steps read naturally in isolation. */ initialWorkspaceName?: string /** Pre-seed invited teammates. */ initialInvites?: string[] class?: string } let { initialStep = 1, initialWorkspaceName = 'Acme Inc', initialInvites = [], class: className }: OnboardingWizardProps = $props() const steps: StepperStepConfig[] = [ { id: 1, title: 'Workspace' }, { id: 2, title: 'Team' }, { id: 3, title: 'Preferences' }, { id: 4, title: 'Done' }, ] // svelte-ignore state_referenced_locally: initial* props seed local state once by design. let step = $state(initialStep) // svelte-ignore state_referenced_locally: initial* props seed local state once by design. let workspaceName = $state(initialWorkspaceName) // svelte-ignore state_referenced_locally: initial* props seed local state once by design. let invites = $state<string[]>([...initialInvites]) let inviteDraft = $state('') let inviteInvalid = $state(false) let preferences = $state<Preference[]>([ { 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, }, ]) const slug = $derived.by(() => { const cleaned = workspaceName.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') return cleaned || 'your-workspace' }) // The stepper only ever emits backward navigation (completed steps are the // clickable ones); still, lock the header once the flow is Done so a stray // click can't move the indicator while the success panel is showing. const stepperSteps = $derived(steps.map((s) => ({ ...s, disabled: step === 4 }))) function addInvite() { const email = inviteDraft.trim() if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || invites.includes(email)) { inviteInvalid = true return } inviteInvalid = false invites.push(email) inviteDraft = '' } function removeInvite(email: string) { invites = invites.filter((e) => e !== email) } function onStepperInput(value: number) { // Free backward navigation; forward goes through the footer button only. if (value < step && step !== 4) step = value } function goNext() { if (step < 4) step += 1 } function reset() { step = 1 workspaceName = '' invites = [] inviteDraft = '' } </script> <SectionCard data-slot="onboarding-wizard" title="Set up your workspace" description="Four quick steps and your team is ready to collaborate." class={className} > <Stepper steps={stepperSteps} value={step} class="mb-6" onValueChange={onStepperInput} /> <!-- Step 1 · Workspace --> {#if step === 1} <div class="space-y-4"> <div class="space-y-2"> <label for="onboarding-workspace-name" class="text-sm font-medium">Workspace name</label> <Input id="onboarding-workspace-name" bind:value={workspaceName} placeholder="Acme Inc" autocomplete="off" /> </div> <div class="border-border bg-muted/40 flex items-center gap-2 rounded-md border px-3 py-2"> <Globe class="text-muted-foreground size-4 shrink-0" aria-hidden="true" /> <p class="min-w-0 truncate text-xs"> <span class="text-muted-foreground">uipkge.dev/</span> <span class="text-foreground font-medium">{slug}</span> </p> </div> </div> {:else if step === 2} <!-- Step 2 · Team --> <div class="space-y-4"> <form class="flex gap-2" onsubmit={(e) => { e.preventDefault() addInvite() }} > <Input bind:value={inviteDraft} type="email" placeholder="[email protected]" status={inviteInvalid ? 'error' : undefined} class="flex-1" onValueChange={() => (inviteInvalid = false)} /> <Button type="submit" variant="outline" size="sm" class="shrink-0"> <Plus aria-hidden="true" /> Add </Button> </form> {#if inviteInvalid} <p role="alert" class="text-destructive text-xs">Enter a valid email address.</p> {/if} {#if invites.length > 0} <div class="flex flex-wrap gap-2"> {#each invites as email (email)} <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" onclick={() => removeInvite(email)} > <X class="size-3" aria-hidden="true" /> </button> </Badge> {/each} </div> {:else} <p class="text-muted-foreground text-xs">No teammates invited yet.</p> {/if} <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" onclick={goNext} > Skip for now </button> </div> {:else if step === 3} <!-- Step 3 · Preferences --> <div class="divide-y rounded-lg border"> {#each preferences as pref (pref.id)} <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> <Switch bind:checked={pref.enabled} aria-label={pref.label} /> </div> {/each} </div> {:else} <!-- Step 4 · Done --> <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" > <Check class="size-8" aria-hidden="true" /> </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"> {#if invites.length > 0} {workspaceName || 'Your workspace'} is ready and {invites.length} invitation{invites.length === 1 ? '' : 's'} sent. {:else} {workspaceName || 'Your workspace'} is ready — invite teammates anytime from settings. {/if} </p> </div> <Button variant="outline" size="sm" onclick={reset}>Set up another workspace</Button> </div> {/if} {#snippet footer()} <div class="flex w-full items-center justify-between"> {#if step > 1 && step < 4} <Button variant="ghost" size="sm" onclick={() => (step -= 1)}> <ChevronLeft aria-hidden="true" /> Back </Button> {:else} <span aria-hidden="true"></span> {/if} <span class="text-muted-foreground text-xs">Step {step} of 4</span> {#if step < 4} <Button size="sm" disabled={step === 1 && workspaceName.trim() === ''} onclick={goNext}> {step === 3 ? 'Finish' : 'Continue'} <ChevronRight aria-hidden="true" /> </Button> {:else} <span aria-hidden="true"></span> {/if} </div> {/snippet} </SectionCard>
Raw manifest:https://uipkge.dev/r/svelte/onboarding-wizard.json