Files
prompts/skills/nicegui-ui-customization/SKILL.md
T
John Lancaster c8906cef7b reorg
2026-06-16 23:39:45 -05:00

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 call services/
  • ui/components/: Reusable presentation components (no business logic)
  • ui/static/: Assets loaded at startup in bootstrap.py
  • services/: 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:

  1. 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")
    
  2. Use Quasar props — high-level component theming built into NiceGUI elements.

    ui.button("Submit").props("color=primary size=lg")
    
  3. 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()) in bootstrap.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/True to prevent duplicate submissions

Asset Caching

  • Inject CSS in bootstrap.py at 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