Build Interactive Python Web UIs with NiceGUI (2026)
Build Interactive Python Web UIs with NiceGUI (2026) is a hands-on guide to shipping dashboards and tools in pure Python. NiceGUI sits on FastAPI with a Vue/Quasar frontend under the hood, so you write ui.label, ui.slider, and ui.button — not a separate React repo. Widgets sync with bind_value, events stay in Python, and ui.run() gives you a browser UI in one file.
If you already explored FastHTML for pure-Python web apps or FastAPI + HTMX for no-build UIs, NiceGUI is the Quasar-styled path when you want rich controls (sliders, notifications, charts) without writing JavaScript. Pair installs with uv + Ruff and you stay in one Python toolchain.
TL;DR
- NiceGUI is a pure-Python UI layer over FastAPI + Vue/Quasar (WebSocket-driven updates).
- Install with
pip install niceguioruv pip install nicegui(we tested3.17.1). - Use
ui.slider/ui.numberwithbind_valuefor two-way sync. - Buttons call Python handlers;
ui.notifyand label updates happen without a JS build. - Prefer NiceGUI for dashboards and internal tools; prefer FastHTML/HTMX for lighter HTML-first pages.
Why NiceGUI in 2026?
Python teams still split into “API in FastAPI, UI in Vue/React.” That works for product teams with frontend specialists. For data apps, admin panels, and ML demos, the split costs more than it buys. NiceGUI keeps the UI in Python while still delivering a modern Quasar look: sliders, dialogs, tables, and live updates over Socket.IO.
| Stack | You write | Best when |
|---|---|---|
| FastAPI + SPA | Python API + JS/TS UI | Heavy client routing, design systems |
| FastAPI + HTMX / FastHTML | Python + HTML partials | Server-driven pages, minimal chrome |
| NiceGUI | Python widgets + events | Dashboards, tools, Quasar-style controls |
Versions tested (2026-09-28)
- Python
3.13.5 nicegui3.17.1- fastapi
0.141.1, starlette1.7.0, uvicorn0.54.0 - Demo server:
http://127.0.0.1:8765(ui.run(..., show=False, reload=False))
python -m venv .venv && source .venv/bin/activate
pip install nicegui
# or: uv pip install nicegui
python app.py
# NiceGUI ready to go on http://127.0.0.1:8765
1. Minimal interactive app
Save this as app.py (under 25 lines). A title, a slider bound to a number input, a counter label, and an Increment button that updates the label and fires a notification:
from nicegui import ui
count = {"n": 0}
level = {"v": 50}
ui.label("NiceGUI Counter Demo").classes("text-h4 text-primary")
ui.label("Slider and number stay in sync via bind_value.")
slider = ui.slider(min=0, max=100, value=level["v"]).props("label-always")
number = ui.number(label="Level", value=level["v"]).bind_value(slider, "value")
count_label = ui.label("Count: 0").classes("text-h5")
def on_inc():
count["n"] += 1
count_label.set_text(f"Count: {count['n']}")
ui.notify(f"Incremented to {count['n']}")
ui.button("Increment", on_click=on_inc).props("color=primary")
ui.run(host="127.0.0.1", port=8765, reload=False, show=False, title="NiceGUI Demo")
number.bind_value(slider, "value") is the key idea: change either control and the other follows. The button never leaves Python — on_click runs on the server and pushes UI updates over the live connection.
2. Real output from the running app
We started the app headlessly and curled the home page. NiceGUI returned HTTP 200 with Quasar/Vue assets under /_nicegui/3.17.1/ and the page JSON contained our widgets:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8765/
# 200
curl -s http://127.0.0.1:8765/ | rg 'title|NiceGUI Counter|Count:|_nicegui/3.17|quasar|q-slider|Increment'
<title>NiceGUI Demo</title>
/_nicegui/3.17.1/static/quasar.umd.prod.js
/_nicegui/3.17.1/static/vue.esm-browser.prod.js
"text":"NiceGUI Counter Demo"
"text":"Count: 0"
"tag":"q-slider" (model-value: 50)
"tag":"q-btn" (label: Increment)
We also exercised the counter and bind sync in pure Python (same logic as the handlers): level starts at 50; setting the number to 72 updates the slider to 72; setting the slider to 33 updates the number to 33; three increments print Count: 1, Count: 2, Count: 3.
3. Practical tips
bind_value. Prefer binding widgets to each other (or to a shared object) instead of manually copying values in every callback.ui.runoptions. Usehost='127.0.0.1',reload=False, andshow=Falsein scripts/CI; open a browser yourself when developing locally.- State. Module dicts are fine for demos. For multi-user apps, keep per-client state (NiceGUI supports client-scoped storage) or back it with a database.
- When to prefer NiceGUI vs FastHTML. Choose NiceGUI for Quasar widgets, plots, and dense dashboards. Choose FastHTML/HTMX when you want minimal HTML pages and classic request/response partials — see our FastHTML tutorial and FastAPI + HTMX guide.
Common pitfalls
- Expecting REST-only updates. NiceGUI’s interactivity is WebSocket/Socket.IO based. Curl proves the shell loaded; clicks update through the live channel.
- Forgetting
bind_value. Without it you must wire both directions by hand — easy to drift. - Blocking the event loop. Heavy work in
on_clickfreezes the UI. Offload to threads/async tasks for long jobs. - Opening ports widely. Default bind may expose the UI on your LAN. Pin
host='127.0.0.1'unless you intend remote access.
What to do next
- Run the demo:
pip install nicegui && python app.py. - Add a second button that resets the counter and notifies “Reset”.
- Bind a
ui.labelto the slider so the level text updates as you drag. - Compare the same dashboard idea in FastHTML if you want HTML-first pages instead of Quasar widgets.
- Keep the toolchain fast with uv + Ruff.
NiceGUI will not replace every SPA — and it does not try to. For interactive Python tools in 2026, writing the UI in Python on top of FastAPI + Quasar is often the shortest path from idea to clickable dashboard.