
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Multi-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.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/onboarding-wizard.json$npx shadcn@latest add https://uipkge.dev/r/react/onboarding-wizard.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/onboarding-wizard.json$bunx shadcn@latest add https://uipkge.dev/r/react/onboarding-wizard.jsonnpx shadcn@latest add @uipkge-react/onboarding-wizardInstalls to:components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
initialStepJump straight to a step (1 Workspace · 2 Team · 3 Preferences · 4 Done). | number | — | optional |
initialWorkspaceNamePre-fill the workspace name so later steps read naturally in isolation. | string | — | optional |
initialInvitesPre-seed invited teammates. | string[] | — | optional |
className | string | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
Preferenceinterface Preference {
id: string
label: string
description: string
enabled: boolean
}'use client'
import * as React from 'react'
import { Check, ChevronLeft, ChevronRight, Globe, Plus, X } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { SectionCard } from '@/components/ui/section-card'
import { Stepper } from '@/components/ui/stepper'
import { Switch } from '@/components/ui/switch'
interface Preference {
id: string
label: string
description: string
enabled: boolean
}
export 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[]
className?: string
}
const steps = [
{ id: 1, title: 'Workspace' },
{ id: 2, title: 'Team' },
{ id: 3, title: 'Preferences' },
{ id: 4, title: 'Done' },
]
const defaultPreferences: 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,
},
]
export function OnboardingWizard({
initialStep = 1,
initialWorkspaceName = 'Acme Inc',
initialInvites = [],
className,
}: OnboardingWizardProps) {
const [step, setStep] = React.useState(initialStep)
const [workspaceName, setWorkspaceName] = React.useState(initialWorkspaceName)
const [invites, setInvites] = React.useState<string[]>(initialInvites)
const [inviteDraft, setInviteDraft] = React.useState('')
const [inviteInvalid, setInviteInvalid] = React.useState(false)
const [preferences, setPreferences] = React.useState<Preference[]>(defaultPreferences)
const slug =
workspaceName
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '') || 'your-workspace'
function addInvite() {
const email = inviteDraft.trim()
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || invites.includes(email)) {
setInviteInvalid(true)
return
}
setInviteInvalid(false)
setInvites((prev) => [...prev, email])
setInviteDraft('')
}
function removeInvite(email: string) {
setInvites((prev) => prev.filter((e) => e !== email))
}
function onStepperInput(value: number) {
// Free backward navigation; forward goes through the footer button only.
if (value < step && step !== 4) setStep(value)
}
function goNext() {
if (step < 4) setStep(step + 1)
}
function reset() {
setStep(1)
setWorkspaceName('')
setInvites([])
setInviteDraft('')
}
return (
<SectionCard
data-slot="onboarding-wizard"
title="Set up your workspace"
description="Four quick steps and your team is ready to collaborate."
className={className}
footer={
<div className="flex w-full items-center justify-between">
{step > 1 && step < 4 ? (
<Button variant="ghost" size="sm" onClick={() => setStep(step - 1)}>
<ChevronLeft aria-hidden="true" />
Back
</Button>
) : (
<span aria-hidden="true" />
)}
<span className="text-muted-foreground text-xs">Step {step} of 4</span>
{step < 4 ? (
<Button size="sm" disabled={step === 1 && workspaceName.trim() === ''} onClick={goNext}>
{step === 3 ? 'Finish' : 'Continue'}
<ChevronRight aria-hidden="true" />
</Button>
) : (
<span aria-hidden="true" />
)}
</div>
}
>
<Stepper steps={steps} value={step} onValueChange={onStepperInput} className="mb-6" />
{step === 1 && (
<div className="space-y-4">
<div className="space-y-2">
<label htmlFor="onboarding-workspace-name" className="text-sm font-medium">
Workspace name
</label>
<Input
id="onboarding-workspace-name"
value={workspaceName}
onChange={(e) => setWorkspaceName(e.target.value)}
placeholder="Acme Inc"
autoComplete="off"
/>
</div>
<div className="border-border bg-muted/40 flex items-center gap-2 rounded-md border px-3 py-2">
<Globe className="text-muted-foreground size-4 shrink-0" aria-hidden="true" />
<p className="min-w-0 truncate text-xs">
<span className="text-muted-foreground">uipkge.dev/</span>
<span className="text-foreground font-medium">{slug}</span>
</p>
</div>
</div>
)}
{step === 2 && (
<div className="space-y-4">
<form
className="flex gap-2"
onSubmit={(e) => {
e.preventDefault()
addInvite()
}}
>
<Input
value={inviteDraft}
onChange={(e) => {
setInviteDraft(e.target.value)
setInviteInvalid(false)
}}
type="email"
placeholder="[email protected]"
status={inviteInvalid ? 'error' : undefined}
className="flex-1"
/>
<Button type="submit" variant="outline" size="sm" className="shrink-0">
<Plus aria-hidden="true" />
Add
</Button>
</form>
{inviteInvalid && (
<p role="alert" className="text-destructive text-xs">
Enter a valid email address.
</p>
)}
{invites.length > 0 ? (
<div className="flex flex-wrap gap-2">
{invites.map((email) => (
<Badge key={email} variant="secondary" className="gap-1 py-1 pr-1 pl-2.5">
{email}
<button
type="button"
aria-label={`Remove ${email}`}
className="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 className="size-3" aria-hidden="true" />
</button>
</Badge>
))}
</div>
) : (
<p className="text-muted-foreground text-xs">No teammates invited yet.</p>
)}
<button
type="button"
className="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>
)}
{step === 3 && (
<div className="divide-y rounded-lg border">
{preferences.map((pref) => (
<div key={pref.id} className="flex items-center justify-between gap-4 p-4">
<div className="min-w-0">
<p className="text-sm font-medium">{pref.label}</p>
<p className="text-muted-foreground mt-0.5 text-xs">{pref.description}</p>
</div>
<Switch
aria-label={pref.label}
checked={pref.enabled}
onCheckedChange={(checked) =>
setPreferences((prev) => prev.map((p) => (p.id === pref.id ? { ...p, enabled: checked } : p)))
}
/>
</div>
))}
</div>
)}
{step === 4 && (
<div className="space-y-4 py-6 text-center">
<div className="border-success/30 bg-success/10 text-success mx-auto flex size-16 items-center justify-center rounded-full border shadow-xs">
<Check className="size-8" aria-hidden="true" />
</div>
<div>
<p className="text-lg font-semibold">You're all set</p>
<p className="text-muted-foreground mx-auto mt-1 max-w-sm text-sm">
{invites.length > 0
? `${workspaceName || 'Your workspace'} is ready and ${invites.length} invitation${invites.length === 1 ? '' : 's'} sent.`
: `${workspaceName || 'Your workspace'} is ready — invite teammates anytime from settings.`}
</p>
</div>
<Button variant="outline" size="sm" onClick={reset}>
Set up another workspace
</Button>
</div>
)}
</SectionCard>
)
}
Raw manifest:https://uipkge.dev/r/react/onboarding-wizard.json