Skip to content

Adding a New Dashboard Page

This guide walks through adding a completely new page to the dashboard, using main.py, layout.py, navbar, and create_control_tab() as the reference pattern.

StepLayer / ResponsibilityFile PathPurpose
1Business Logicapp/services/my_tab_logic.pyData processing, async operations, state updates
2UI Widget Buildersapp/ui/my_tab_ui.pyDefines and returns UI components (no logic)
3Page Assembly & Wiringapp/ui/components/my_tab.pyBuilds UI, initializes state, wires events
4Navigation Integrationapp/ui/components/navbar.pyAdds the page to the navbar
5Route Registrationapp/main.pyRegisters the page and layout

Create app/services/my_tab_logic.py. This module contains all data operations, state management, and async functions. It must not import anything from app/ui/ — the logic layer has no knowledge of the UI.

"""Logic for the My Tab page."""
from app.backend.logger_config import setup_logger
logger = setup_logger(__name__)
async def load_my_data(app_state: dict, my_ui) -> None:
"""Load data and update the UI components.
Args:
app_state (dict): Shared application state dict.
my_ui: UI namespace object containing widget references.
"""
try:
my_ui.status_label.text = "Data loaded"
except Exception as e:
logger.exception("Failed to load data")
my_ui.status_label.text = f"Error: {e}"

Functions receive app_state and a UI namespace object — they never build UI themselves.

Create app/ui/my_tab_ui.py. This module builds NiceGUI widgets and returns namespace objects with widget references attached. It must not contain any business logic or async calls.

"""UI builders for the My Tab page."""
from nicegui import ui
from app.ui.theme import Theme
theme = Theme()
def create_my_tab(app_state: dict):
"""Build the My Tab UI and return a namespace with widget references.
Args:
app_state (dict): Shared application state dict.
Returns:
object: Namespace object with all widget references attached.
"""
with ui.card().tight().classes(theme.card_classes()):
ui.label("My Tab").classes(theme.section_header_classes()).style(
f"color: {theme.colors['primary']}"
)
status_label = ui.label("Ready").classes("text-sm text-gray-500")
load_btn = ui.button(
"Load Data", icon="download", color="primary"
).props("unelevated")
content_container = ui.column().classes("w-full mt-4")
class _MyTab:
pass
tab = _MyTab()
tab.status_label = status_label
tab.load_btn = load_btn
tab.content_container = content_container
return tab

The namespace pattern (a plain class with attributes attached after construction) is the same pattern used in create_model_run() and all other UI builders. Use it consistently.

Create app/ui/components/my_tab.py. This is the only file that imports from both app/ui/ and app/services/.

"""Event wiring for the My Tab page."""
import asyncio
from nicegui import ui
from app.ui.my_tab_ui import create_my_tab
from app.services.my_tab_logic import load_my_data
from app.ui.theme import Theme
theme = Theme()
def create_my_tab_page():
app_state = {
"my_data": None,
"my_data_loaded": False,
}
my_ui = create_my_tab(app_state)
my_ui.load_btn.on_click(
lambda: asyncio.create_task(load_my_data(app_state, my_ui))
)
ui.timer(
0.3,
lambda: asyncio.create_task(load_my_data(app_state, my_ui)),
once=True,
)

Always use ui.timer(..., once=True) for initial data loads rather than calling async functions directly at module level. This ensures the NiceGUI page has finished rendering before data starts loading.

Open app/ui/components/navbar.py and add an entry for your page:

ui.link("My Tab", "/my-tab").classes(
nav_link_classes("my_tab", active)
)

Open app/main.py and add a new @ui.page route:

from app.ui.components.my_tab import create_my_tab_page
from app.ui.components.section_header import section_header
from app.ui.components.navbar import navbar
from app.ui.components.layout import page_container
from app.ui.theme import apply_theme_once
@ui.page("/my-tab")
def my_tab_page():
apply_theme_once()
navbar("my_tab")
page_container(lambda: (
section_header(
"My Tab Title",
"A short description shown below the title on the page.",
),
create_my_tab_page(),
))

page_container wraps your content in the full-width themed layout. section_header renders the page title and subtitle. apply_theme_once() must be the first call in every page function.

□ app/services/my_tab_logic.py
□ All data operations are async
□ Functions accept app_state dict and a UI namespace object
□ No imports from app/ui/
□ app/ui/my_tab_ui.py
□ Builder function returns a namespace object with all widget refs
□ No async calls or data fetching
□ Uses theme.card_classes(), theme.section_header_classes() etc.
□ app/ui/components/my_tab.py
□ All event handlers wired with asyncio.create_task()
□ Initial data load uses ui.timer(..., once=True)
□ app/ui/components/navbar.py
□ New nav entry added with correct route and active key
□ app/main.py
□ @ui.page("/my-tab") route defined
□ apply_theme_once() called first
□ navbar(), page_container(), section_header() all used

After completing these steps, restart the application and your new page will appear in the navigation bar at http://localhost:8000/my-tab.