Skip to content

Onboarding wizard

A four-step setup wizard: a vertical stepper with progress, a profile form, a workspace name with selectable template cards, team invites and a summary before launching. Each step validates before moving on.

Onboarding wizardOnboarding wizard

The screenshot follows the site’s theme: switch between light and dark at the top of the page to see the block in One Light and One Dark.

From the repository root:

Terminal window
python examples/blocks/onboarding.py
  • The current step is plain state (self.step); each step is a method that returns its view.
  • Selectable cards built from pz.container(...).style(...).on(click=...).
  • The stepper is drawn with icons, pz.rule and theme tokens, so it follows any theme.
  • Back / Continue / Skip buttons and an error callout.
Component Used for
Progress Overall progress
Input Text fields
Select Role
Badge Step number
Card Invites and summary
List item Invited people
Empty state No invites yet
Callout Validation errors
Button Navigation

The whole block, ready to copy. It only needs pip install pyzed-ui.

examples/blocks/onboarding.py
"""Onboarding block: a multi-step setup wizard with a stepper, choice cards, validation and a summary."""
import pyzed as pz
from pyzed import ui
STEPS = [
("user", "Your profile", "Tell us who you are"),
("layout-template", "Workspace", "Pick a template"),
("users", "Invite your team", "Work together from day one"),
("rocket", "All set", "Review and launch"),
]
TEMPLATES = [
("layout-dashboard", "Dashboard", "KPIs, charts and tables for internal tools."),
("bot", "AI assistant", "A streaming chat app wired to any LLM."),
("gamepad-2", "Game", "A canvas, a game loop and keyboard input."),
("code", "Developer tool", "A code editor with docks and a terminal."),
]
ROLES = ["Developer", "Designer", "Data scientist", "Student", "Founder"]
class Onboarding(pz.App):
title = "Welcome"
size = (1200, 760)
def __init__(self):
self.step = 1
self.name = "Ada Lovelace"
self.role = "Developer"
self.template = "AI assistant"
self.workspace = "analytical-engine"
self.invites = ["charles@babbage.dev"]
self.invite = ""
self.error = ""
# -- actions ---------------------------------------------------------------------------
def next(self):
if self.step == 0 and not self.name.strip():
self.error = "Please enter your name."
return
if self.step == 1 and not self.workspace.strip():
self.error = "Your workspace needs a name."
return
self.error = ""
self.step = min(self.step + 1, len(STEPS) - 1)
def back(self):
self.error, self.step = "", max(self.step - 1, 0)
def add_invite(self):
if "@" in self.invite and self.invite not in self.invites:
self.invites.append(self.invite.strip())
self.invite = ""
# -- pieces ----------------------------------------------------------------------------
def stepper(self):
items = []
for i, (icon, title, subtitle) in enumerate(STEPS):
done, current = i < self.step, i == self.step
marker = pz.container(
pz.icon("check" if done else icon, size=16, color="#ffffff" if done or current else None),
width=36, height=36, center=True,
).style(background="accent" if done or current else "element", radius=18,
border=(2, "accent" if current else "border_variant"))
items.append(pz.row([
marker,
pz.column([ui.label(title, weight="semibold" if current else None,
color="text" if done or current else "text_muted"),
ui.muted(subtitle, size="sm")], spacing=2),
], spacing=12, align="center"))
if i < len(STEPS) - 1:
items.append(pz.container(pz.rule(vertical=True, thickness=2), height=28, padding=(0, 0, 0, 17)))
return pz.column([
pz.row([pz.container(pz.icon("zap", size=16, color="#ffffff"), padding=6).style(
background="accent", radius=8),
ui.label("Acme", size="lg", weight="semibold")], spacing=10, align="center"),
pz.space(height=24),
pz.column(items, spacing=4),
pz.vspace(),
ui.muted("Step {} of {}".format(self.step + 1, len(STEPS)), size="sm"),
ui.progress(self.step + 1, max=len(STEPS), height=4),
], spacing=8, width=290, height="fill", padding=32).style(background="panel")
def template_card(self, icon, title, text):
selected = title == self.template
return pz.container(
pz.column([
pz.row([pz.container(pz.icon(icon, size=20), padding=8).style(
background="accent" if selected else "element", radius=10,
color="#ffffff" if selected else "text"),
pz.hspace(),
pz.icon("circle-check", size=18) if selected else None], align="start"),
ui.label(title, weight="semibold"),
ui.muted(text, size="sm"),
], spacing=10),
padding=16, width="fill", height=150,
).style(background="surface", radius=12, border=(2 if selected else 1, "accent" if selected else "border"),
).on(click=lambda: setattr(self, "template", title), cursor="pointer")
def profile_step(self):
return pz.column([
ui.field("Full name", pz.container(ui.input(self.name, lambda v: setattr(self, "name", v)), width=340)),
ui.field("What do you do?", ui.select(ROLES, self.role, lambda v: setattr(self, "role", v), width=340)),
], spacing=16)
def workspace_step(self):
cards = [self.template_card(*t) for t in TEMPLATES]
return pz.column([
pz.column([ui.label("Workspace name", size="sm", weight="medium"),
ui.input(self.workspace, lambda v: setattr(self, "workspace", v), icon="at-sign",
size="lg")], spacing=6),
pz.column([ui.label("Start from a template", size="sm", weight="medium"),
pz.row(cards[:2], spacing=14), pz.row(cards[2:], spacing=14)], spacing=10),
], spacing=22)
def team_step(self):
people = [ui.list_item(email, None, icon="mail", end=ui.icon_button(
"x", lambda email=email: self.invites.remove(email), size="sm")) for email in self.invites]
return pz.column([
pz.row([ui.input(self.invite, lambda v: setattr(self, "invite", v), placeholder="teammate@company.com",
icon="mail", on_submit=self.add_invite, size="lg"),
ui.button("Add", self.add_invite, size="lg")], spacing=8),
ui.card(pz.column(people, spacing=2) if people else ui.empty_state("No invites yet", icon="users"),
padding=6),
], spacing=14)
def summary_step(self):
rows = [("Name", self.name), ("Role", self.role), ("Workspace", f"@{self.workspace}"),
("Template", self.template), ("Invites", str(len(self.invites)))]
return ui.card(pz.column([pz.row([ui.muted(k), pz.hspace(), ui.label(v, weight="medium")]) for k, v in rows],
spacing=12))
def view(self):
icon, title, subtitle = STEPS[self.step]
body = [self.profile_step, self.workspace_step, self.team_step, self.summary_step][self.step]()
last = self.step == len(STEPS) - 1
content = pz.column([
pz.column([ui.badge(f"Step {self.step + 1}", tint="accent"),
ui.heading(["Nice to meet you", "Create your workspace", "Bring your team",
"You're ready to go"][self.step]),
ui.muted(["We'll tailor Acme to how you work.",
"A workspace holds your projects, people and settings. You can change it later.",
"Invite people now or skip this step; they'll get an email.",
"Here's what we'll set up for you."][self.step])], spacing=8),
body,
ui.callout(self.error, tint="error", icon="circle-alert") if self.error else None,
pz.vspace(),
pz.row([
ui.button("Back", self.back, icon="arrow-left", variant="ghost", disabled=self.step == 0),
pz.hspace(),
ui.button("Skip", self.next, variant="ghost") if self.step == 2 else None,
ui.button("Launch workspace" if last else "Continue", self.next, variant="solid", size="lg",
icon="rocket" if last else "arrow-right", icon_position="start" if last else "end"),
], spacing=8, align="center"),
], spacing=26, max_width=640, height="fill", padding=(48, 56))
return pz.row([self.stepper(), ui.divider(vertical=True),
pz.container(content, width="fill", height="fill", align_x="center")],
height="fill").style(background="background")
if __name__ == "__main__":
pz.run(Onboarding)

Change theme (or self.theme = pz.themes.get("One Dark").with_accent("#e11d48")), replace the sample data with your own and split pieces into components when they grow. An AI agent with the MCP server can read this block with read_example("blocks/onboarding") and adapt it for you.