11 KiB
name, description
| name | description |
|---|---|
| nicegui-ui-customization | Style, customize, and build interactive UIs in a production-ready NiceGUI app. |
NiceGUI UI Customization & Component Patterns
Customize, style, and build interactive user interfaces in a production-ready NiceGUI application. This skill covers component architecture, styling systems, event-driven interactions, and practical troubleshooting for production deployments.
Goal
Create a well-architected, responsive, accessible UI that:
- Separates structure (Python layout) from cosmetics (CSS styling)
- Handles user interactions via event-driven patterns, not polling
- Composes reusable components with clear responsibilities
- Integrates custom styling and assets cleanly
- Troubleshoots common failures (race conditions, asset caching, upload errors)
Project Structure & Conceptual Boundaries
This skill assumes a project organized with clear, modular boundaries:
src/app/
├─ ui/
│ ├─ pages/ # Page-level UI modules (route handlers)
│ │ ├─ __init__.py
│ │ ├─ home.py
│ │ ├─ dashboard.py
│ │ └─ about.py
│ ├─ components/ # Reusable presentation components
│ │ ├─ __init__.py
│ │ ├─ nav.py
│ │ ├─ card_section.py
│ │ └─ form_section.py
│ └─ static/ # Static assets (CSS, JS, images)
│ ├─ css/
│ │ ├─ base.css
│ │ ├─ theme.css
│ │ └─ components.css
│ ├─ js/
│ └─ images/
├─ services/ # Business logic layer
│ ├─ __init__.py
│ ├─ file_service.py
│ ├─ user_service.py
│ └─ ...
├─ api/ # HTTP endpoints
│ ├─ __init__.py
│ ├─ health.py
│ └─ routes.py
├─ bootstrap.py # App composition, CSS/asset setup
├─ config.py # Settings and configuration
└─ ...
Key boundaries:
ui/pages/: Route-level adapters that callservices/ui/components/: Reusable presentation components (no business logic)ui/static/: Assets loaded at startup inbootstrap.pyservices/: Business logic (independent of UI)api/: HTTP endpoints
Dependency flow: pages/ → components/ + services/ (no reverse imports)
UI Component Patterns
Components are the building blocks of reusable UI. They live in ui/components/ and are consumed by pages in ui/pages/. Each component maps to NiceGUI elements, which expose a consistent API for structure, styling, and event handling.
Reusable Components
Extract to ui/components/ when patterns repeat across pages. Keep in-page layouts unique to that page.
# ui/components/card_section.py
def card_section(title: str, content: str) -> ui.card:
with ui.card().classes("w-full max-w-md") as card:
ui.label(title).classes("text-lg font-bold")
ui.label(content).classes("text-gray-600")
return card
Layout Patterns
Use Tailwind for all structural styling. Reserve .style() for computed values.
with ui.column().classes("w-full"):
with ui.row().classes("w-full gap-4 flex-wrap sm:flex-nowrap"):
ui.card().classes("flex-1 min-w-64")
ui.card().classes("flex-1 min-w-64")
State Management
Use bindable dataclasses for automatic two-way binding.
from nicegui import binding, ui
from dataclasses import field
@binding.bindable_dataclass
class PageState:
selected_id: int | None = None
items: list = field(default_factory=list)
state = PageState()
detail_label = ui.label().bind_text_from(state, 'selected_id')
def on_select(item_id):
state.selected_id = item_id # Auto-updates UI
state.bind_on_change(lambda: load_detail(), 'selected_id')
Styling
Use Tailwind for all styling. Use breakpoint prefixes (sm:, md:, lg:) for responsive design.
Custom CSS Injection ⚠️ Avoid This Pattern
Avoid injecting custom CSS via ui.add_css() at app startup. Custom CSS injection creates maintenance burden, breaks IDE autocomplete, and makes styling harder to reason about. Instead:
✅ Prefer these approaches:
-
Use Tailwind utility classes — covers 95% of styling needs with zero CSS overhead.
with ui.column().classes("w-full bg-gradient-to-br from-gray-100 to-gray-200"): with ui.card().classes("rounded-lg border border-gray-300 shadow-md hover:shadow-lg transition-shadow"): ui.label("Content") -
Use Quasar props — high-level component theming built into NiceGUI elements.
ui.button("Submit").props("color=primary size=lg") -
Extract reusable components — encapsulate styling patterns in Python functions rather than CSS classes.
def styled_card(title: str, content: str) -> ui.card: with ui.card().classes("rounded-lg border border-gray-300 shadow-md") as card: ui.label(title).classes("text-lg font-bold") ui.label(content).classes("text-gray-600") return card
If you absolutely must inject CSS (rare edge cases only):
- Store CSS in
src/app/static/css/files - Load via
ui.add_css(open('src/app/static/css/custom.css').read())inbootstrap.py - Keep injected CSS minimal and well-documented
- Avoid hardcoding colors or spacing; use CSS variables instead
Static Assets
Load CSS files at app startup in bootstrap.py:
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="src/app/static"), name="static")
ui.add_css(open('src/app/static/css/base.css').read())
Use CSS variables for design tokens:
:root {
--color-primary: #3b82f6;
--spacing-unit: 1rem;
}
JS Injection (Rare)
Use ui.run_javascript() only for unsupported interactions. Prefer NiceGUI APIs first.
Event-Driven Patterns
NiceGUI provides event-driven APIs for actions and events. This section covers common patterns: file uploads, form submissions, WebSocket/SSE for real-time updates, and background tasks.
File Upload
Validate and notify; delegate storage to service layer.
async def handle_upload(e: ui.events.UploadEventArguments):
try:
if e.size > 10 * 1024 * 1024:
raise ValueError("File too large")
if not e.name.endswith('.pdf'):
raise ValueError("Only PDF allowed")
await file_service.store(e.content.read(), e.name)
ui.notify(f"Uploaded: {e.name}", type="positive")
except ValueError as err:
ui.notify(str(err), type="negative")
ui.upload(on_upload=handle_upload, auto_upload=True)
Form Submission
Bind form inputs to dataclass; validate in service.
@binding.bindable_dataclass
class FormData:
name: str = ""
email: str = ""
data = FormData()
ui.input("Name").bind_value(data, 'name')
ui.input("Email").bind_value(data, 'email')
async def on_submit():
try:
await user_service.create_user(name=data.name, email=data.email)
ui.notify("User created", type="positive")
data.name = data.email = ""
except ValueError as err:
ui.notify(str(err), type="negative")
ui.button("Submit").on_click(on_submit)
Real-Time Updates (WebSocket / SSE)
For streaming updates, use SSE (one-way) or WebSocket (bidirectional).
# api/events.py
@app.get("/events/status")
async def status_stream():
async def gen():
while True:
yield f"data: {await get_status()}\\n\\n"
await asyncio.sleep(1)
return StreamingResponse(gen(), media_type="text/event-stream")
# Page
status_label = ui.label()
async def listen():
async with aiohttp.ClientSession() as s:
async with s.get("/api/events/status") as r:
async for line in r.content:
if line.startswith(b"data:"):
status_label.set_text(line.decode().replace("data:", "").strip())
ui.timer(0.1, lambda: asyncio.create_task(listen()))
Background Tasks
Delegate long-running work to background tasks; signal UI via polling or WebSocket.
# API
@app.post("/process")
async def process(background_tasks: BackgroundTasks):
background_tasks.add_task(long_job)
return {"status": "processing"}
# Page
async def on_start():
await client.post("/api/process")
ui.notify("Started")
async def check():
status = await client.get("/api/process/status")
if status == "done":
ui.notify("Complete", type="positive")
ui.button("Start").on_click(on_start)
ui.timer(1, check)
Reactive Refresh
Use @ui.refreshable with explicit refresh triggers; avoid polling everything.
@ui.refreshable
async def item_list():
items = await service.list()
for item in items:
ui.label(item.name)
ui.button("Refresh").on_click(lambda: item_list.refresh())
Advanced Interactions
Dialogs
dialog = ui.dialog()
async def show_confirm():
with dialog:
ui.label("Are you sure?")
ui.button("Yes").on_click(lambda: (confirm_action(), dialog.close()))
ui.button("No").on_click(dialog.close)
dialog.open()
ui.button("Delete").on_click(show_confirm)
Progress
progress = ui.linear_progress(value=0).classes("w-full")
async def handle_upload(e):
for chunk in read_chunks(e.content):
progress.set_value(uploaded / total)
ui.notify("Complete", type="positive")
Drag & Drop
Use Quasar q-sortable for native drag-and-drop support via .props().
Troubleshooting
Upload Validation & Errors
- Validate file size/extension before storage
- Catch exceptions and emit
ui.notify()with type "negative" - Log errors for debugging
Race Conditions
- Serialize updates:
await service.get_data()before UI update - Avoid multiple timers on same component
- Use
button.enabled = False/Trueto prevent duplicate submissions
Asset Caching
- Inject CSS in
bootstrap.pyat startup, not per-page - Clear browser cache (Ctrl+Shift+Del) or use cache-busting query params:
?v=hash - Verify
/static/mount path and nginx proxy rewrites
Navigation & State
- Don't store state in globals; keep in
services/or request-scoped - Use
ui.navigate.to()to switch pages - Reload data in destination page's
@ui.page()decorator