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.
Overview of What You Will Create
Section titled “Overview of What You Will Create”| Step | Layer / Responsibility | File Path | Purpose |
|---|---|---|---|
| 1 | Business Logic | app/services/my_tab_logic.py | Data processing, async operations, state updates |
| 2 | UI Widget Builders | app/ui/my_tab_ui.py | Defines and returns UI components (no logic) |
| 3 | Page Assembly & Wiring | app/ui/components/my_tab.py | Builds UI, initializes state, wires events |
| 4 | Navigation Integration | app/ui/components/navbar.py | Adds the page to the navbar |
| 5 | Route Registration | app/main.py | Registers the page and layout |
Step 1 — Create the Tab Logic Module
Section titled “Step 1 — Create the Tab Logic Module”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.
Step 2 — Create the UI Builder Module
Section titled “Step 2 — Create the UI Builder Module”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 uifrom 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 tabThe 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.
Step 3 — Create the Wiring Module
Section titled “Step 3 — Create the Wiring Module”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 asynciofrom nicegui import uifrom app.ui.my_tab_ui import create_my_tabfrom app.services.my_tab_logic import load_my_datafrom 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.
Step 4 — Add a Navbar Entry
Section titled “Step 4 — Add a Navbar Entry”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))Step 5 — Register the Route in main.py
Section titled “Step 5 — Register the Route in main.py”Open app/main.py and add a new @ui.page route:
from app.ui.components.my_tab import create_my_tab_pagefrom app.ui.components.section_header import section_headerfrom app.ui.components.navbar import navbarfrom app.ui.components.layout import page_containerfrom 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.
Checklist
Section titled “Checklist”□ 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 usedAfter completing these steps, restart the application and your new page
will appear in the navigation bar at http://localhost:8000/my-tab.