How to run a spaday app in Pyodide¶
This guide shows you how to run Python UI logic in a Web Worker and apply its component-tree patches on the browser’s main thread.
Build the wheel and browser runtime¶
Install the development dependencies once, then build the runtime and Pyodide wheel:
make develop
make build-js
make test-pyodide
The Python 3.14 WebAssembly wheel is written to dist/pyodide/.
Open the example¶
Serve the repository root:
python -m http.server 8000
Open the example with the wheel URL in its query string, replacing <wheel> with the filename in
dist/pyodide/:
http://127.0.0.1:8000/js/examples/pyodide.html?wheel=/dist/pyodide/<wheel>
Click Increment in Python. The Python handler sends a transports client proposal through its
authoritative session, renders the accepted model as a new component tree, and returns the diff.
connectWorker applies that patch without replacing the button.
The Python application is in spaday/examples/pyodide.py. The
worker loader is in js/examples/pyodide-worker.js, and the page
is in js/examples/pyodide.html.
Connect your own worker¶
Wrap a render function and intent handler with WorkerApp:
from spaday import SendPatch, WorkerApp, element, lit
count = 0
def render():
return element("button").text(str(count)).on(
"click", SendPatch("counter", "increment", lit(1))
)
def on_intent(intent):
global count
count += intent["detail"]["value"]
app = WorkerApp(render, on_intent)
The worker must send app.start_json() after receiving {type: "start"} and pass later messages to
app.dispatch_json(...). On the main thread, initialize spaday’s browser runtime and connect the
worker:
import { connectWorker, init } from "/js/dist/esm/index.js";
await init({ module_or_path: "/js/dist/pkg/spaday_bg.wasm" });
const worker = new Worker("/worker.js", { type: "module" });
await connectWorker(document.querySelector("#app"), worker).ready;
Keep DOM and custom-element work on the main thread. Keep Python rendering, state changes, and tree diffing in the worker.
Run the browser test¶
Run the focused Playwright test against the wheel already in dist/pyodide/:
make test-pyodide-browser
Browse the standalone examples¶
Open the standalone gallery to run the focused examples without installing Python. Use the picker to switch among seven core examples. Peer packages host their own standalone examples; for example, open spaday-trees in Pyodide.
The page installs the Python wheels in a Pyodide Web Worker and loads each wheel’s browser assets on
the main thread. When an example exports a transports Server, the runner detects it and uses
postMessage as an in-browser wire:
browser Store → transports JS Client → Web Worker → Python Server/Session
← authoritative accepted patch ←
This preserves Python model validation, background updates, and server-authoritative edits. No HTTP or WebSocket server is created.
Use the deployed server examples for behavior that inherently crosses browser processes: shared state
between tabs or users, clustering, SSR, host-framework mounting, and external services. When Pyodide
does connect to a deployed service, transports already detects sys.platform == "emscripten" and
implements Client.connect() with the browser’s native WebSocket or EventSource.
Run the notebook examples in JupyterLite¶
Both ends WebAssembly: the Pyodide kernel runs your Python, and the notebook widget (which bundles the spaday runtime and wasm core) renders the tree in the notebook frontend — no server. Open the interactive examples and run all cells. The notebook runs these packaged examples directly:
widget.pyfor rendering and client-side actions;devices.pyfor reactive device controls;
The standalone worker example runs
pyodide.py with fully interactive Python-owned state and
rendering. Its worker loads the Pyodide wheel published with the documentation site.
make jupyterlite # builds the site into dist/lite (wheel + demo notebook included)
make test-jupyterlite # or: drive the site's REPL in Chromium end-to-end
Serve dist/lite from any static host. The build copies the core wheel, dependency wheels and browser
assets, standalone runner, and notebook into the site. The spaday wheel installs from the site’s own wheel index
(%pip install spaday anywidget); anywidget and pydantic come from PyPI. Widget frontend
extensions cannot be %pip installed at runtime — the site build bundles them (jupyterlab_widgets
for the ipywidgets manager plus anywidget; see the jupyterlite Make target). A Lite site built
without them shows the widget’s text repr instead of the rendered tree.
If a previously visited site misbehaves after a redeploy (e.g. an old wheel version, or missing files): JupyterLite caches hard — a service worker plus browser storage can keep serving the previous build’s kernel and packages. Hard refresh (Cmd/Ctrl+Shift+R), or clear the site’s data (service worker + IndexedDB) and reload.