Skip to content

Custom components

A component is a class that groups widgets with their logic, like a React component:

  • Props are annotated class attributes with defaults. The parent passes them on every render.
  • State is any other attribute you set. It stays between renders.
  • render() returns the component’s widgets.
  • events declares the custom events it raises with self.emit(name, value).
import pyzed as pz
class Rating(pz.Component):
"""Stars the user can click."""
value: int = 0 # props
max: int = 5
events = {"change": "A star was picked (event.value is the rating)."}
icon = "star" # how it appears in the designer's toolbox
def setup(self): # once, before the first render
self.hover = None # state
def render(self):
shown = self.hover or self.value
return pz.row([
pz.text("★" if i <= shown else "☆", size=20).on(
click=lambda i=i: self.emit("change", i),
mouse_enter=lambda i=i: setattr(self, "hover", i),
mouse_leave=lambda: setattr(self, "hover", None),
)
for i in range(1, self.max + 1)
], spacing=2)

Calling a component returns a widget, so it goes anywhere a widget does:

class Review(pz.App):
stars = 3
def view(self):
return pz.column([
Rating(value=self.stars, on_change=lambda e: setattr(self, "stars", e.value)),
pz.text(f"{self.stars} of 5"),
])

Events and components

See examples/components.py for a star rating, a tag editor and a draggable note.

  • Custom events are declared in events, as a tuple of names or a {name: description} dict. The parent handles them with on_<name>=.
  • self.emit(name, value) calls the parent’s handler with a pz.Event whose value and source are set. Return its result from your callback, so a Command or a message reaches the runtime.
  • Common events, such as on_click, on_drag and on_key_down, work on every component without any code. See Events.
  • Lifecycle events: on_mount and on_unmount tell the parent when the component appears and disappears.
  • style= styles the component like any widget (Rating(value=3, style="card")), unless it declares a style prop of its own. See Styles.

State can also be declared on the class with pz.state(default). Unlike a prop, the parent cannot pass it, every instance starts with its own copy of the default (so lists and dicts are safe), and the designer lists it next to the props:

class Counter(pz.Component):
step: int = 1 # prop
count: int = pz.state(0) # state
def render(self):
return ui.button(f"{self.count}", lambda: setattr(self, "count", self.count + self.step))

State is kept per position in the parent’s view(): the second Rating in a view keeps its own state as long as the order stays the same. When components come from a list whose items move, give each one a stable key:

pz.column([TodoItem(key=todo.id, todo=todo) for todo in self.todos])

A component that is no longer rendered is unmounted, and its state is dropped.

flowchart LR
    View["App.view()"] -->|"Rating(value=3)"| Slot{"Instance at this<br/>position or key?"}
    Slot -->|no| New["new instance<br/>setup()"]
    Slot -->|yes| Same["same instance<br/>(state kept)"]
    New --> Props["props set"]
    Same --> Props
    Props --> Changed{"props, state or<br/>context changed?"}
    Changed -->|yes| Render["render() → widgets"]
    Changed -->|no| Last["last drawing, reused"]
    Render -->|first time| Mount["on_mount()"]
    View -.->|not rendered again| Unmount["on_unmount()"]

render() only runs when something it depends on changed: its props, its state, the context it reads, the theme or a component inside it. Otherwise the last drawing is reused, so a view with hundreds of components stays fast. If render() reads something else (the time, a global), call self.refresh() when it changes or set cache = False on the class. See components drawn only when they change.

Components nest like HTML elements and React components:

class Card(pz.Component):
title: str = ""
children: list = () # the positional arguments, like JSX children
stylesheet = { # CSS-like rules for this component (& is the component)
"&": {"background": "panel", "padding": 16, "radius": 8, "border": (1, "border")},
"&:hover": {"border_color": "accent"},
}
def render(self):
return pz.box(pz.text(self.title).style(weight="bold"), *self.children, gap=8)
Card(pz.text("Three new messages"), ui.button("Open", self.open), title="Inbox",
class_="wide", animate="fade_in_up")
  • A prop called children takes the positional arguments. Lists are flattened, and None and False are skipped.
  • stylesheet adds rules to pz.stylesheet when the class is defined. & stands for the component: "&", "&:hover", "&.wide".
  • Every component takes style=, class_=, id= and animate= (presets for widget.animate), unless it declares props with those names.
  • Context shares values with every component rendered inside, without passing props down each level:
with pz.provide(user=self.user):
page = Dashboard() # any component inside reads it
class Avatar(pz.Component):
def render(self):
return ui.avatar(pz.use_context("user").name) # or self.context("user")

A component can also share values with what it renders by returning them from a provide() method. Children passed in as arguments are rendered before the component, so they see the context around it, not its own. See examples/web_layout.py for a page built this way.

Hook When
setup() Once, before the first render. Props are already set. Initialize state here.
render() On every render of the parent.
on_mount() After the first render. It may return a Command or a message.
on_unmount() When the component is no longer rendered.

Hooks run after the view is drawn, never in the middle of it.

Every pz.Component subclass appears in the designer’s toolbox:

  • Props become properties. bool gets a switch, int and float a number box, Literal[...] a drop-down, list one item per line, and a prop named color or icon gets its own picker. Unions, optionals, dicts, sets and tuples (int | None, dict[str, int]…) get an editor built from the type, with a row per item. The type hints of pyzed pick a richer editor, like export hints in Godot:

    Annotation Editor
    pz.Color A color picker (theme token or #rrggbb).
    pz.Icon An icon picker.
    pz.Multiline A text area.
    Annotated[int, pz.Range(0, 10)] A number box limited to 0 to 10 (step is optional).

    They are plain str and int at run time: tint: pz.Color = "accent".

  • Custom events become events you can write code for.

Class attributes control how it appears:

Attribute Default
label The class name, split into words.
icon "component" (any Lucide icon).
category "Custom".
size (160, 40), the size when placed on a form.
designer True; False keeps it out of the toolbox.
type_name The class name in snake case; the id saved in forms.

The docstring’s first paragraph is shown as the description.

To build a component by drawing it instead of writing render(), use the designer’s component mode.