In this tutorial, we build a fully interactive, multi-page web application using NiceGUI. We start by setting up the environment and designing a reusable layout that includes navigation, theming, and dark mode support. As we move forward, we implement a live dashboard with real-time metrics and charts, demonstrating reactive bindings and timed updates. We then extend the application with a complete CRUD-based todo system, followed by a validated form with dialogs and user feedback mechanisms. We also incorporate file upload functionality with dynamic previews and conclude the feature set with an asynchronous chat interface that simulates real-time interaction. Also, we ensure that the app runs seamlessly in Colab by using background threading and dynamic port allocation.
import sys
import subprocess
subprocess.run([sys.executable, “-m”, “pip”, “install”, “-q”, “nicegui”], check=True)
import threading, time, random, asyncio, base64, socket
from datetime import datetime
from nicegui import ui, events
class State:
def __init__(self):
self.todos = [
{“id”: 1, “task”: “Explore NiceGUI”, “done”: True, “priority”: “High”},
{“id”: 2, “task”: “Build a dashboard”, “done”: False, “priority”: “Medium”},
{“id”: 3, “task”: “Deploy to production”, “done”: False, “priority”: “Low”},
]
self.next_id = 4
self.metrics = {“users”: 1247, “revenue”: 8420, “orders”: 53}
self.series = [random.uniform(20, 80) for _ in range(20)]
self.messages = [{“role”: “assistant”,
“text”: “Hi! Type something and I will echo it back.”}]
state = State()
def page_shell(active: str) -> None:
dark = ui.dark_mode()
drawer = ui.left_drawer(value=True).classes(“bg-grey-2”)
with drawer:
ui.label(“Navigation”).classes(“text-lg font-bold p-2”)
for label, path, icon in [
(“Dashboard”, “/”, “dashboard”),
(“Todos”, “/todos”, “check_circle”),
(“Form”, “/form”, “edit_note”),
(“Upload”, “/upload”, “upload_file”),
(“Chat”, “/chat”, “chat”),
]:
cls = “w-full” + (” bg-primary text-white” if label == active else “”)
ui.button(label,
on_click=lambda p=path: ui.navigate.to(p),
icon=icon).classes(cls).props(“flat align=left no-caps”)
with ui.header(elevated=True).classes(“items-center justify-between bg-primary”):
with ui.row().classes(“items-center”):
ui.button(on_click=drawer.toggle, icon=”menu”).props(“flat color=white”)
ui.label(” NiceGUI Tutorial”).classes(“text-xl font-semibold text-white”)
ui.button(icon=”dark_mode”, on_click=dark.toggle).props(“flat color=white”)
with ui.footer().classes(“bg-grey-3 text-black justify-center”):
ui.label(“Built with NiceGUI · Tutorial Demo”)
We install and import all required libraries, then initialize our application state. We define a central State class to manage todos, metrics, chart data, and chat messages across the app. We also built a reusable layout function that provides navigation, a header, a footer, and dark mode support for all pages.
@ui.page(“/”)
def dashboard():
page_shell(“Dashboard”)
with ui.column().classes(“w-full p-6 gap-6”):
ui.label(“Live Dashboard”).classes(“text-3xl font-bold”)
with ui.row().classes(“gap-4 flex-wrap”):
for key, label, color, icon in [
(“users”, “Users”, “primary”, “group”),
(“revenue”, “Revenue”, “positive”, “attach_money”),
(“orders”, “Orders”, “warning”, “shopping_cart”),
]:
with ui.card().classes(“w-60”):
with ui.row().classes(“items-center justify-between w-full”):
ui.label(label).classes(“text-gray-500”)
ui.icon(icon, size=”md”).classes(f”text-{color}”)
ui.label().classes(f”text-3xl font-bold text-{color}”)
.bind_text_from(state.metrics, key, backward=lambda v: f”{v:,}”)
with ui.card().classes(“w-full”):
ui.label(“Live stream (updates every second)”).classes(“text-lg font-semibold”)
chart = ui.echart({
“tooltip”: {“trigger”: “axis”},
“xAxis”: {“type”: “category”, “data”: list(range(len(state.series)))},
“yAxis”: {“type”: “value”},
“series”: [{“data”: list(state.series), “type”: “line”,
“smooth”: True, “areaStyle”: {}}],
}).classes(“h-64 w-full”)
def tick():
state.series.append(random.uniform(20, 80))
state.series.pop(0)
chart.options[“series”][0][“data”] = list(state.series)
chart.update()
state.metrics[“users”] += random.randint(-2, 4)
state.metrics[“revenue”] += random.randint(-100, 200)
state.metrics[“orders”] = max(0, state.metrics[“orders”] + random.randint(-1, 3))
ui.timer(1.0, tick)
We create the dashboard page and structure it with responsive UI components. We bind metric cards directly to the state to enable automatic updates and display real-time values. We also implement a live chart using ECharts and dynamically update both the chart and the metrics using a timer.
@ui.page(“/todos”)
def todos_page():
page_shell(“Todos”)
with ui.column().classes(“w-full p-6 gap-4 max-w-4xl mx-auto”):
ui.label(“Todos”).classes(“text-3xl font-bold”)
with ui.card().classes(“w-full”):
with ui.row().classes(“w-full items-center gap-2”):
task_input = ui.input(placeholder=”What needs doing?”).classes(“flex-grow”)
priority_sel = ui.select([“Low”, “Medium”, “High”], value=”Medium”).classes(“w-36”)
def add_todo():
if not task_input.value or not task_input.value.strip():
ui.notify(“Task cannot be empty”, type=”warning”); return
state.todos.append({
“id”: state.next_id,
“task”: task_input.value.strip(),
“done”: False,
“priority”: priority_sel.value,
})
state.next_id += 1
task_input.value = “”
todo_list.refresh()
ui.notify(“Added!”, type=”positive”)
ui.button(“Add”, icon=”add”, on_click=add_todo).props(“color=primary”)
task_input.on(“keydown.enter”, add_todo)
@ui.refreshable
def todo_list():
if not state.todos:
ui.label(“Nothing here yet “).classes(“text-gray-500”); return
for todo in state.todos:
with ui.card().classes(“w-full”):
with ui.row().classes(“w-full items-center gap-3”):
ui.checkbox(value=todo[“done”],
on_change=lambda e, t=todo: t.update(done=e.value))
lbl = ui.label(todo[“task”]).classes(“flex-grow text-lg”)
if todo[“done”]:
lbl.style(“text-decoration: line-through; opacity: 0.5”)
color = {“High”: “red”, “Medium”: “orange”, “Low”: “green”}[todo[“priority”]]
ui.badge(todo[“priority”], color=color)
def make_del(t=todo):
def _del():
state.todos.remove(t)
todo_list.refresh()
ui.notify(“Removed”, type=”info”)
return _del
ui.button(icon=”delete”, on_click=make_del())
.props(“flat color=red round dense”)
todo_list()
We implement a complete CRUD-based todo system with add, update, and delete functionality. We handle user input validation and dynamically refresh the UI using NiceGUI’s refreshable components. We also enhance the UI with checkboxes, badges, and notifications to improve interactivity and feedback.
@ui.page(“/form”)
def form_page():
page_shell(“Form”)
with ui.column().classes(“w-full p-6 max-w-2xl mx-auto gap-4”):
ui.label(“Profile Form”).classes(“text-3xl font-bold”)
with ui.card().classes(“w-full gap-2”):
name = ui.input(“Name”, validation={“Required”: lambda v: bool(v)})
email = ui.input(“Email”, validation={“Must be an email”: lambda v: “@” in (v or “”)})
age = ui.number(“Age”, value=18, min=0, max=120)
ui.label(“Subscription plan”).classes(“mt-2 text-gray-600”)
plan = ui.radio([“Free”, “Pro”, “Enterprise”], value=”Free”).props(“inline”)
agree = ui.checkbox(“I accept the terms”)
async def submit():
if not (name.value and “@” in (email.value or “”) and agree.value):
ui.notify(“Please fix the form first”, type=”negative”); return
with ui.dialog() as d, ui.card():
ui.label(“Submitted!”).classes(“text-xl font-bold”)
ui.label(f”Name: {name.value}”)
ui.label(f”Email: {email.value}”)
ui.label(f”Age: {age.value}”)
ui.label(f”Plan: {plan.value}”)
ui.button(“OK”, on_click=d.close).props(“color=primary”)
d.open()
ui.button(“Submit”, on_click=submit).props(“color=primary”)
@ui.page(“/upload”)
def upload_page():
page_shell(“Upload”)
with ui.column().classes(“w-full p-6 max-w-3xl mx-auto gap-4”):
ui.label(“File Upload”).classes(“text-3xl font-bold”)
result = ui.column().classes(“w-full”)
def handle_upload(e: events.UploadEventArguments):
content = e.content.read()
with result:
with ui.card().classes(“w-full”):
ui.label(f” {e.name}”).classes(“font-semibold”)
ui.label(f”Size: {len(content):,} bytes · type: {e.type}”)
if e.type and e.type.startswith(“image/”):
b64 = base64.b64encode(content).decode()
ui.image(f”data:{e.type};base64,{b64}”).classes(“w-64 rounded”)
else:
try:
ui.code(content[:500].decode(“utf-8″, errors=”replace”))
except Exception:
pass
ui.notify(f”Uploaded {e.name}”, type=”positive”)
ui.upload(on_upload=handle_upload, multiple=True, auto_upload=True).classes(“w-full”)
We build a form with validation rules and handle submission using an asynchronous function. We display user input in a dialog upon successful submission and ensure proper validation before processing. We also implement a file upload feature that supports multiple files and provides image previews and content previews for other file types.
@ui.page(“/chat”)
def chat_page():
page_shell(“Chat”)
with ui.column().classes(“w-full p-6 max-w-3xl mx-auto gap-4”):
ui.label(“Chat (echo bot)”).classes(“text-3xl font-bold”)
@ui.refreshable
def chat_log():
for m in state.messages:
ui.chat_message(
m[“text”],
name=”You” if m[“role”] == “user” else “Bot”,
sent=m[“role”] == “user”,
stamp=datetime.now().strftime(“%H:%M”),
)
with ui.card().classes(“w-full”):
chat_log()
async def send():
text = (entry.value or “”).strip()
if not text:
return
state.messages.append({“role”: “user”, “text”: text})
entry.value = “”
chat_log.refresh()
await asyncio.sleep(1)
reply = f’You said: “{text}” — that is {len(text)} characters!’
state.messages.append({“role”: “assistant”, “text”: reply})
chat_log.refresh()
with ui.row().classes(“w-full items-center”):
entry = ui.input(placeholder=”Type a message…”)
.classes(“flex-grow”).on(“keydown.enter”, send)
ui.button(icon=”send”, on_click=send).props(“color=primary round”)
def _free_port() -> int:
s = socket.socket()
s.bind((“”, 0))
port = s.getsockname()[1]
s.close()
return port
PORT = _free_port()
def _serve():
ui.run(host=”0.0.0.0″, port=PORT, reload=False, show=False,
title=”NiceGUI Tutorial”, storage_secret=”colab-demo”)
threading.Thread(target=_serve, daemon=True).start()
time.sleep(4)
try:
from google.colab import output
from google.colab.output import eval_js
output.serve_kernel_port_as_iframe(PORT, height=”850″)
print(f”App running on port {PORT}”)
print(“Open in a new browser tab:”)
print(eval_js(f”google.colab.kernel.proxyPort({PORT})”))
except ImportError:
print(f”Not in Colab — open http://localhost:{PORT} in your browser”)
We develop an asynchronous chat interface that simulates real-time interaction. We manage chat messages in the shared state and dynamically refresh the chat log after each message. Also, we configure the app to run on a dynamically selected free port in a background thread and expose it within the Colab environment.
In conclusion, we developed a comprehensive understanding of how to build and structure modern web applications using NiceGUI. We brought together multiple advanced concepts such as state management, reactive UI updates, routing, asynchronous workflows, and real-time visualization into a single cohesive system. We also addressed practical challenges, such as running web servers in notebook environments and ensuring component reusability. This end-to-end implementation demonstrates the flexibility and power of NiceGUI and also equips us with the skills to prototype, test, and scale interactive applications efficiently.
Check out the Full Codes with Notebook here. Also, feel free to follow us on Twitter and don’t forget to join our 130k+ ML SubReddit and Subscribe to our Newsletter. Wait! are you on telegram? now you can join us on telegram as well.
Need to partner with us for promoting your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar etc.? Connect with us
The post How to Build a Fully Interactive Multi-Page NiceGUI Application with Real-Time Dashboard, CRUD Operations, File Upload, and Async Chat appeared first on MarkTechPost.