Earlier last year, I needed to build an internal dashboard for the DevOps team to monitor server status and deployment logs. Streamlit felt too limiting with its layout, Dash’s callback pattern was more complex than I expected, and Flask + Jinja meant writing extra JavaScript — the last thing I wanted when I was deep in Python work.
After some searching, I gave NiceGUI a shot — and it’s been running in production for 6 months now. The result: a stable dashboard, a happy team, and not a single line of JavaScript written.
What Is NiceGUI and Why Is It Different?
NiceGUI is a Python library for building web interfaces — dashboards, forms, chat UIs, monitoring tools — entirely in Python. Under the hood it uses FastAPI as the web server and Vue.js + Quasar as the frontend framework, but you don’t need to know any of that while coding.
What sets it apart from similar tools:
- Streamlit: Re-renders the entire page on every change — not ideal for real-time data.
- Dash: Complex callback patterns that become hard to debug as the app grows.
- Gradio: Focused on ML demos, not general-purpose dashboards.
- NiceGUI: Reactive UI, real-time updates over WebSocket, flexible layouts, rich component library.
NiceGUI works on an event-driven model: you define UI components in Python, bind them to data or functions, and when data changes, the UI updates automatically without reloading the page.
Installation and Getting Started
Installing NiceGUI is straightforward:
pip install nicegui
Your first Hello World app:
from nicegui import ui
ui.label('Hello, NiceGUI!')
ui.button('Click me', on_click=lambda: ui.notify('You just clicked!'))
ui.run()
Run the file, open http://localhost:8080, and you’re done. NiceGUI handles the server setup and auto-reloads when code changes in dev mode.
Hands-on: A Real-time Server Monitoring Dashboard
Here’s a practical example — a dashboard displaying CPU, RAM, and disk usage, updating every 2 seconds:
pip install nicegui psutil
import psutil
from nicegui import ui
# Cards displaying metrics
with ui.row().classes('w-full gap-4 p-4'):
with ui.card().classes('flex-1'):
ui.label('CPU Usage').classes('text-lg font-bold')
cpu_label = ui.label('0%').classes('text-3xl text-blue-500')
with ui.card().classes('flex-1'):
ui.label('RAM Usage').classes('text-lg font-bold')
ram_label = ui.label('0%').classes('text-3xl text-green-500')
with ui.card().classes('flex-1'):
ui.label('Disk Usage').classes('text-lg font-bold')
disk_label = ui.label('0%').classes('text-3xl text-orange-500')
# CPU history chart
with ui.card().classes('w-full p-4'):
ui.label('CPU History (60s)').classes('font-bold mb-2')
cpu_chart = ui.chart({
'title': False,
'chart': {'type': 'line'},
'series': [{'name': 'CPU %', 'data': [0] * 60}],
'xAxis': {'visible': False},
'yAxis': {'min': 0, 'max': 100},
}).classes('w-full h-64')
cpu_history = [0] * 60
def update_metrics():
cpu = psutil.cpu_percent()
ram = psutil.virtual_memory().percent
disk = psutil.disk_usage('/').percent
cpu_label.set_text(f'{cpu:.1f}%')
ram_label.set_text(f'{ram:.1f}%')
disk_label.set_text(f'{disk:.1f}%')
cpu_history.pop(0)
cpu_history.append(cpu)
cpu_chart.options['series'][0]['data'] = cpu_history
cpu_chart.update()
ui.timer(2.0, update_metrics)
ui.run(title='Server Monitor', port=8080)
ui.timer() calls update_metrics() every 2 seconds — NiceGUI pushes updates to the browser via WebSocket automatically, with no client-side polling needed. This is its biggest advantage over Streamlit.
Server Table with Sorting
from nicegui import ui
columns = [
{'name': 'server', 'label': 'Server', 'field': 'server', 'sortable': True},
{'name': 'status', 'label': 'Status', 'field': 'status'},
{'name': 'uptime', 'label': 'Uptime', 'field': 'uptime', 'sortable': True},
]
rows = [
{'server': 'web-01', 'status': '✅ Online', 'uptime': '15d 3h'},
{'server': 'web-02', 'status': '✅ Online', 'uptime': '7d 11h'},
{'server': 'db-01', 'status': '⚠️ Warning', 'uptime': '30d 2h'},
]
ui.table(columns=columns, rows=rows, row_key='server').classes('w-full')
ui.run()
Input Form with Validation
from nicegui import ui
with ui.card().classes('p-4 w-96'):
ui.label('Add new server').classes('text-xl font-bold mb-4')
hostname = ui.input('Hostname', placeholder='web-01.example.com').classes('w-full')
port = ui.number('Port', value=22, min=1, max=65535).classes('w-full')
env = ui.select(
label='Environment',
options=['production', 'staging', 'development'],
value='production'
).classes('w-full')
def submit():
if not hostname.value:
ui.notify('Hostname cannot be empty!', type='warning')
return
ui.notify(f'Added {hostname.value}:{port.value} ({env.value})', type='positive')
ui.button('Add', on_click=submit).classes('w-full mt-4')
ui.run()
Parsing Logs with Regex — A Small Trick That Saves Time
The trickiest part of building a log viewer dashboard is getting the regex pattern right to extract information from log lines. I usually test patterns quickly on toolcraft.app/en/tools/developer/regex-tester first — paste in a few real log lines, confirm the matches look right, then copy the pattern into Python. Much faster than running and re-running a script to debug.
Example: parsing an nginx access log and displaying it in a NiceGUI table:
import re
from nicegui import ui
LOG_PATTERN = re.compile(
r'(?P<ip>\d+\.\d+\.\d+\.\d+) .+ \[(?P<time>[^\]]+)\] '
r'"(?P<method>\w+) (?P<path>[^ ]+) HTTP/[\d.]+" '
r'(?P<status>\d+) (?P<bytes>\d+)'
)
def parse_log_line(line: str) -> dict | None:
m = LOG_PATTERN.match(line)
return m.groupdict() if m else None
sample_logs = [
'192.168.1.1 - - [15/Jul/2026:10:00:01 +0900] "GET /api/status HTTP/1.1" 200 1234',
'10.0.0.2 - - [15/Jul/2026:10:00:05 +0900] "POST /api/deploy HTTP/1.1" 201 567',
]
entries = [e for l in sample_logs if (e := parse_log_line(l))]
columns = [
{'name': 'ip', 'label': 'IP', 'field': 'ip'},
{'name': 'method', 'label': 'Method', 'field': 'method'},
{'name': 'path', 'label': 'Path', 'field': 'path'},
{'name': 'status', 'label': 'Status', 'field': 'status'},
]
ui.table(columns=columns, rows=entries, row_key='path').classes('w-full')
ui.run()
Production Deployment
NiceGUI runs behind nginx like any standard FastAPI app. You’ll need to add WebSocket support to your nginx config:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
And a systemd service for automatic restarts on crash:
[Unit]
Description=NiceGUI Dashboard
After=network.target
[Service]
User=ubuntu
WorkingDirectory=/opt/dashboard
ExecStart=/opt/dashboard/venv/bin/python main.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl enable dashboard
sudo systemctl start dashboard
Conclusion: When Should You Use NiceGUI?
After 6 months of real-world use, I can say it plainly: NiceGUI is best suited for internal tools and internal dashboards — the kind of things you need to build quickly, need real-time updates, and where the team doesn’t have a dedicated frontend engineer. If you’re building a SaaS product with complex UX and heavy animations, stick with React or Vue.
Its real strengths:
- All logic stays in Python — no context-switching to JS/CSS
- Real-time updates over WebSocket out of the box, no extra configuration
- Complete component library: charts, tables, forms, dialogs, notifications
- Tailwind CSS classes applied directly via
.classes() - Integrates with an existing FastAPI app if you need to scale up
A few caveats: apps with many concurrent users require careful state management since each connection is its own session. The plugin ecosystem is also not as broad as Streamlit’s. But if you’re looking to build a dashboard quickly for your team without learning another JS framework, NiceGUI is well worth trying.

