Skip to main content

nicegui-pyodide

Run stock NiceGUI entirely in the browser via Pyodide / PyScript — no server, no websocket, no backend — as an external extension.

No fork. No patches to NiceGUI's source. This package makes an unmodified pip install nicegui importable and runnable under Pyodide from the outside, by weaving a set of sys.modules shims in before import nicegui.

This is the extension re-implementation of the upstream Pyodide effort (zauberzeug/nicegui#5776), which required editing ~25 core files. Here that lives entirely in a separate package instead.

Why an extension instead of a PR?

The PR approach had to touch NiceGUI core in ~25 places — wrapping every from fastapi import … / from socketio import … in try/except ImportError and adding if IS_PYODIDE: branches. That is invasive and hard to merge.

Pyodide has no server: there is no uvicorn, no socket.io, and NiceGUI's server-only imports (fastapi, starlette, uvicorn, aiofiles, anyio, ifaddr) simply don't exist. So a plain import nicegui explodes before any of your code runs.

This extension solves it from outside core:

  1. A sys.meta_path finder fabricates a permissive stub module for every import under those absent packages — rich enough (subclassable, callable, decorator-friendly, operator-safe) that stock NiceGUI's unguarded usages don't crash: class App(FastAPI), @app.post(url), class APIRouter(fastapi.APIRouter), class CacheControlledStaticFiles(StaticFiles), Jinja2Templates(path). (nicegui_pyodide/_shims.py)
  2. A lazy nicegui.nicegui seed supplies the app singleton without running stock nicegui/nicegui.py — which would build the FastAPI app + socket.io server + HTTP routes. Stock nicegui/__init__.py does from .nicegui import app; the import machinery finds the seed first.
  3. An in-process JS bridge replaces socket.io: Python emit() calls hop straight to JavaScript over the Pyodide FFI. (bridge.py, outbox.py, runtime.py)
  4. A pyodide-patched nicegui.js (shipped as an asset, pinned to a NiceGUI version) that talks to the bridge instead of a websocket. All new paths are gated on window.niceguiBridge, so the file is backward-compatible with server mode.

On a normal (server) Python this package is inert — import nicegui behaves exactly as usual — so shared code can import it unconditionally.

Usage

pip install nicegui-pyodide            # pulls in a compatible nicegui
nicegui-pyodide-build ./dist --serve   # build + serve → http://localhost:8080

--serve builds and serves in one step; without it, nicegui-pyodide-build ./dist just assembles the dir and you serve it however you like (python -m http.server -d ./dist 8080).

Edit dist/app.py to change the UI, then reload the page. Re-running nicegui-pyodide-build ./dist refreshes the generated files but keeps your app.py (pass --force to reset it to the starter template). The app-building pattern:

import nicegui_pyodide                 # MUST come before `import nicegui`
from nicegui import Client, ui
from nicegui_pyodide import page

with Client(page('/')) as client:
    ui.label('Hello from the browser!')
    ui.button('Click me', on_click=lambda: ui.notify('Clicked!'))
# the generated entrypoint mounts `client` via PyodideRuntime

For coding agents / LLMs

This package uses a different idiom from server-mode NiceGUI. If you know NiceGUI, these are the things you'll get wrong on the first try:

  • Build the UI in a with Client(page('/')) as client: block — do not call ui.run() or ui.run_with(). There is no server to start; both raise a RuntimeError telling you this. @ui.page server routes don't exist either.
  • import nicegui_pyodide MUST come before import nicegui (it installs the import shims). Keep the # noqa: F401 on it so linters don't "helpfully" delete the line, and don't let an import-sorter move it below import nicegui. Getting this wrong raises a clear RuntimeError rather than failing silently.
  • The workflow is build-a-static-bundle, not python app.py: nicegui-pyodide-build ./dist --serve, then edit dist/app.py and reload.
  • No backend features: app.add_static_files, HTTP endpoints, native/multiprocessing modes are unavailable. Root-relative asset paths (/foo.png) won't resolve — use absolute URLs or data:/blob: URIs. See the support matrix below.

A copy of these rules also lives in AGENTS.md.

Element support matrix

Most NiceGUI elements are pure client-side Vue and work unchanged. A handful assume a live server (a socket.io connection or a component static route) and therefore need special handling in the browser. Status against the pinned NiceGUI release:

Element Status Notes
labels, buttons, inputs, layout, tables, most Quasar wrappers ✅ works pure client-side; no server assumption
ui.markdown (incl. Mermaid, code highlighting) ✅ works (override) codehilite CSS served from ./dynamic_resources/ instead of a server route
ui.scene / ui.scene_view (3D / WebGL) ✅ works (shim) the init handshake polled window.socket.id; a minimal window.socket stand-in is provided in Pyodide mode. Browser-verified (WebGL canvas renders)
ui.leaflet (map) ✅ works (override) same init shim, plus CSS/JS (leaflet, optional leaflet-draw) resolved from ./esm/nicegui-leaflet/. Browser-verified (map + tiles render)
ui.xterm (terminal) ✅ works (override) xterm.css resolved from ./esm/nicegui-xterm/ instead of a server route. Browser-verified (terminal renders)
ui.echart with a theme URL string ⚠️ partial inline theme objects work; a theme passed as a URL fetch()es a server path — it fails soft (theme just doesn't apply). Use an inline theme object
ui.image / ui.interactive_image / ui.video / ui.audio / ui.link with a root-relative src/href (e.g. /foo.png) ⚠️ partial such paths were app.add_static_files / uploaded-file routes with no backend here. Use absolute URLs, data:/blob: URIs, or bundle the asset. External URLs are unaffected
@ui.page server routes, app.add_static_files, HTTP endpoints, native mode, multiprocessing ❌ unsupported require a real backend; no-op or unavailable in the browser (use the Client(page(...)) pattern shown above)

The overridden/shimmed elements (scene, leaflet, xterm) are the socket.io/handshake and static-route cases that would otherwise fail with an opaque TypeError (window.socket is undefined) or a silent missing-stylesheet; all three are covered by the browser smoke test. Genuinely server-bound features (last row) are documented as unsupported rather than patched.

Version pinning

nicegui_pyodide/static/nicegui.js is a pyodide-patched copy of a specific NiceGUI release's nicegui.js. The nicegui>=3.14,<3.15 pin in pyproject.toml keeps them in lockstep. To support a newer NiceGUI, regenerate the patched JS against that release and widen the pin.

Status / limitations

  • Verified in-browser (Chromium via Playwright): render, button click → handler → DOM update, ui.notify, input round-trip through the bridge, ui.upload round-trip, on_connect handlers, and app.storage.tab.
  • File upload works client-side: files are read in the browser (FileReader → base64) and delivered over the bridge to runtime._handle_upload (no server route).
  • @ui.page server routes are not available (there is no server); use the Client(page(...)) pattern shown above.
  • Anything needing a real backend (HTTP endpoints, app.add_static_files, multiprocessing, native mode) is a no-op or unavailable in the browser.

License & attribution

This project is MIT-licensed (see LICENSE), Copyright (c) 2026 evnchn.

It bundles modified copies of a few files from NiceGUI (MIT, © Zauberzeug GmbH): nicegui_pyodide/static/nicegui.js and the component overrides under nicegui_pyodide/static/component_overrides/ (markdown.js, leaflet/leaflet.js, xterm/xterm.js). See NOTICE for details. Other vendor assets (Quasar, Vue, Tailwind, …) are copied from your own installed NiceGUI at build time and are not redistributed in this repository.

The Pyodide mechanism re-implemented here originated in the author's own upstream PR, zauberzeug/nicegui#5776 (closed).

Metadata

Release files for nicegui-pyodide 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nicegui-pyodide 0.1.1
File Size Uploaded
nicegui_pyodide-0.1.1.tar.gz 45.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nicegui-pyodide 0.1.1
File Interpreter ABI Platform
nicegui_pyodide-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 89.3 kB

Release files / nicegui_pyodide-0.1.1.tar.gz

Download URL nicegui_pyodide-0.1.1.tar.gz
Size 45.2 kB
Tags Source
SHA-256 checksum
How to use checksums
e865eb27bad37da07bfed927faa280ef35c2f0d8f18a4a3fded955ebd8d57d5b
BLAKE2b-256 checksum
How to use checksums
f3e9ef1f9f5ec89376852ef6370404f1646109d6c2a79523d2497eb0d6b73273
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.11

Release files / nicegui_pyodide-0.1.1-py3-none-any.whl

Download URL nicegui_pyodide-0.1.1-py3-none-any.whl
Size 44.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5baaf1ca0108a95a5ec5233e83a458360fbe1e78c8e8900fd5079efe11fcaa08
BLAKE2b-256 checksum
How to use checksums
174c0f86a067426c3e0455c8da24965dc8b379bba89f34248a3bb32bccf70522
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.11

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page