Build Interactive Python Web UIs with NiceGUI (2026)

Web Development Sep 28, 2026

Watch on YouTube

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 nicegui or uv pip install nicegui (we tested 3.17.1).
  • Use ui.slider / ui.number with bind_value for two-way sync.
  • Buttons call Python handlers; ui.notify and 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.

StackYou writeBest when
FastAPI + SPAPython API + JS/TS UIHeavy client routing, design systems
FastAPI + HTMX / FastHTMLPython + HTML partialsServer-driven pages, minimal chrome
NiceGUIPython widgets + eventsDashboards, tools, Quasar-style controls

Versions tested (2026-09-28)

  • Python 3.13.5
  • nicegui 3.17.1
  • fastapi 0.141.1, starlette 1.7.0, uvicorn 0.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.run options. Use host='127.0.0.1', reload=False, and show=False in 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_click freezes 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

  1. Run the demo: pip install nicegui && python app.py.
  2. Add a second button that resets the counter and notifies “Reset”.
  3. Bind a ui.label to the slider so the level text updates as you drag.
  4. Compare the same dashboard idea in FastHTML if you want HTML-first pages instead of Quasar widgets.
  5. 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.

Share:
Last updated: September 2026
Built-in Functions in Python 2026 str() in Python 2026: String Conversion + Modern Formatting & Best Practices memoryview with JAX in Python 2026: Zero-Copy NumPy → JAX Array Interop + Efficient ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + GPU Pinning & ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + ML Examples memoryview with NumPy & PyTorch in Python 2026: Zero-Copy Views, Efficient Slicing & ML Interop Examples memoryview with NumPy in Python 2026: Zero-Copy Views, Efficient Slicing & Real ML Examples memoryview() in Python 2026: Zero-Copy Magic for Large Binary Data + Real Examples import() in Python 2026: How It Works, Security Risks & Modern Alternatives (importlib) zip() in Python 2026: How to Use It, strict=True Behavior & Real-World Examples vars() in Python 2026: Accessing Object Namespace + Modern Introspection Patterns type() in Python 2026: Dynamic Type Inspection & Object Creation + Modern Patterns tuple() in Python 2026: Immutable Sequences + Modern Patterns & Best Practices super() in Python 2026: Method Resolution & Modern Inheritance Patterns sum() in Python 2026: Summing Iterables + Modern Numeric Patterns & Best Practices staticmethod() in Python 2026: Static Methods, Utility Functions & Modern Best Practices sorted() in Python 2026: Sorting Iterables + Modern Patterns & Best Practices slice() in Python 2026: Creating Slice Objects + Modern Patterns & Best Practices setattr() in Python 2026: Dynamic Attribute Setting + Modern Patterns & Safety set() in Python 2026: Mutable Sets Creation + Modern Patterns & Best Practices