NI

NiceGUI

Python UI framework for web-based dashboards and apps

Developer Tools & Git ★ 16.2k stars Easy setup MIT

NiceGUI is a Python framework for creating web-based user interfaces and dashboards with minimal code, running on a built-in server. It is ideal for self-hosted internal tools and hardware control panels.

Key features

  • Web UIs in pure Python
  • Built-in web server
  • Live updates
  • Great for dashboards

Pros & cons

Strengths

  • Pure Python UIs
  • Built-in live updates
  • Great for hardware panels

Trade-offs

  • Not for complex frontends
  • State lives server-side

NiceGUI replaces

Last reviewed Aug 26, 2026 · 837 words

from nicegui import ui
ui.label('Hello, homelab')
ui.run()

That is a complete web application on port 8080, with a websocket back to Python so the page updates when your code does. NiceGUI (MIT, Python, 16,162 stars) is the framework I reach for when a self-hosted tool needs a control panel and I refuse to write JavaScript: buttons, charts, tables, switches and log viewers in plain Python, comfortable in 256 MB of RAM. It replaces Streamlit for anything that must react to hardware or a running process rather than re-run a script from the top.

Event-driven beats script-rerun for control panels

Streamlit executes your whole script again on every click, which is a fine model for a data exploration page and a bad one for a panel with a "restart service" button and a live status line. NiceGUI is FastAPI underneath with a Vue and Quasar front end, and each browser tab holds a persistent socket.io connection. Elements are Python objects you mutate, and the change is pushed to the browser. A status panel is a timer and two labels:

import psutil
from nicegui import ui

cpu = ui.label()
mem = ui.label()

def refresh():
    cpu.text = f'CPU {psutil.cpu_percent():.0f}%'
    mem.text = f'RAM {psutil.virtual_memory().percent:.0f}%'

ui.timer(2.0, refresh)
ui.run(port=8080, title='Box status')

Async handlers are supported directly, so an on_click that awaits a subprocess or an HTTP call does not block the other tabs. ui.log gives you a scrolling log pane, ui.echart and ui.plotly cover charts, ui.table and ui.aggrid cover data, and the Quasar component set underneath means the result looks like software rather than a notebook. Gradio sits in the same family but is shaped around wrapping a model with inputs and outputs; for anything that is not an ML demo, NiceGUI is the more general tool.

The Home Assistant and hardware pattern

The reason the framework is popular with homelabbers is that it talks to anything Python can reach. A button that toggles a light through the Home Assistant REST API is 12 lines:

import httpx
from nicegui import ui

HA = 'http://homeassistant.lan:8123'
TOKEN = 'a-long-lived-access-token'

async def toggle(entity: str):
    async with httpx.AsyncClient() as c:
        await c.post(f'{HA}/api/services/light/toggle',
                     headers={'Authorization': f'Bearer {TOKEN}'},
                     json={'entity_id': entity})

ui.button('Office light', on_click=lambda: toggle('light.office'))
ui.run()

Swap httpx for an MQTT client and the same shape drives a Zigbee relay; swap it for pyserial and it is a front panel for an Arduino. Home Assistant's own dashboards are better for the whole-house view, and the dashboards category covers link boards like Homepage and Dashy, which are a different animal entirely. NiceGUI is for the panel that does not exist yet: the one specific to your 3D printer farm, your backup jobs, or the one script three people in the house need to run without SSH.

Deploying it like any other service

The official zauberzeug/nicegui image mounts your code at /app and runs it; a five-line Dockerfile on python:3.12-slim with pip install nicegui is just as good. Set ui.run(host='0.0.0.0', reload=False) in a container, because the auto-reloader is for development. Behind Caddy it is a one-line reverse_proxy nicegui:8080, and Caddy passes the websocket upgrade through without extra configuration. If you use app.storage.user to remember per-visitor state, pass storage_secret to ui.run or the framework refuses to start. There is no built-in login: put Caddy basic auth or a forward-auth provider in front of anything that presses buttons on real hardware, and keep the port off the public internet regardless.

Where it stops

All state lives on the server, per connected client, in one Python process. That is fine for a household and a small team and wrong for a public site with hundreds of concurrent users. Custom components beyond the shipped set mean writing Vue, at which point you are back in JavaScript. A dashboard with heavy client-side interaction, drag-and-drop layouts or offline behaviour is not what this is for. "On Air", the relay that exposes a local app through a public URL, is a paid Zauberzeug service; a reverse proxy or Tailscale does the same job without it.

What I'd do

Write the panel as a single main.py, run it in the official image behind Caddy with basic auth, and connect it to Home Assistant over its API rather than duplicating device logic. Use ui.timer for status, async handlers for actions, and ui.log for the output of whatever you trigger. Reach for Streamlit when the deliverable is a chart to explore, Gradio when it is a model to demo, and NiceGUI when it is a thing to control.

Compare NiceGUI

2 head-to-head comparisons.

Similar developer tools & git apps