Files
prompts/src/personal_mcp/docs/skills/nicegui/references/styling-and-customization.md
T
2026-08-07 23:59:03 -05:00

16 KiB

NiceGUI Styling And Customization

Use this reference to discover how a NiceGUI component can be customized, apply the least invasive supported mechanism, and introduce CSS without fighting Quasar's internal geometry.

For package boundaries, dependency direction, and page or component ownership, load application architecture.

Progressive Customization Workflow

Increase the customization level only when the previous source does not expose what the design requires:

  1. Read the NiceGUI documentation page for the component.
  2. Inspect the NiceGUI element function or class constructor.
  3. Identify the wrapped Quasar component and read its documentation.
  4. Use Quasar props, slots, and events through NiceGUI's native customization APIs.
  5. Use Tailwind classes for structural layout.
  6. Add a scoped static stylesheet for stable visual fine tuning.

Stop as soon as the required behavior is supported. Do not begin by targeting Quasar's generated DOM or internal selectors.

1. Start With The NiceGUI Component Page

Find the component in the NiceGUI documentation. Check its examples, parameters, methods, events, bindings, and inheritance before writing CSS. The component page establishes the public NiceGUI API and often demonstrates the intended Quasar integration.

Confirm the target project's installed NiceGUI version because the current online documentation can differ from the pinned release.

2. Inspect The NiceGUI Constructor

Read the signature and implementation of the imported NiceGUI function or element class. The constructor reveals accepted Python parameters, defaults, event callbacks, validation, and values NiceGUI forwards to the frontend.

Use editor navigation or runtime inspection against the project's selected environment:

from inspect import getsource, signature

from nicegui import ui

print(signature(ui.select))
print(getsource(ui.select))

When ui.<name> is a factory or alias, follow it to the element class in the NiceGUI element sources. Prefer the installed package source when behavior may differ by version.

3. Read The Underlying Quasar Component Docs

NiceGUI wraps Quasar components such as QInput, QSelect, and QDialog. Use the matching Quasar component page to discover its complete props, slots, events, methods, and behavior notes.

Map Quasar's Vue API onto the NiceGUI wrapper instead of copying a Vue template. Verify that a prop or slot exists in the Quasar version used by the installed NiceGUI release.

4. Apply Native Quasar Features Through NiceGUI

Use the NiceGUI element customization methods to reach the supported Quasar surface:

  • .props(...) for Quasar properties and boolean flags
  • .classes(...) for Tailwind utilities and stable application class names
  • .style(...) for dynamic inline values or a quick, local probe
  • .on(...) for events that are not represented by a constructor callback
  • slots or child elements for Quasar extension points exposed by the wrapper
with ui.select(
    options=items,
    label="Item",
).props(
    "outlined clearable options-dense popup-content-class=app-item-menu"
).classes(
    "w-full md:max-w-md"
) as item_select:
    with item_select.add_slot("prepend"):
        ui.icon("inventory_2")

Prefer constructor arguments when NiceGUI exposes the behavior directly. Use .props() for supported Quasar features that are not constructor parameters. Use slots when the Quasar docs define a semantic insertion point; do not reproduce that content with absolute positioning.

Application Themes With NiceGUI And Quasar

Treat a theme as three related layers with different owners:

  1. Configure Quasar's named color roles through NiceGUI.
  2. Let Quasar own light, dark, and automatic mode state.
  3. Define application semantic tokens for surfaces and content not covered by Quasar components.

Do not implement a parallel theme switch by replacing Quasar classes or directly restyling each component. NiceGUI's color APIs set the supported Quasar --q-* custom properties, so Quasar components, color= arguments, and classes such as text-primary and bg-positive stay aligned.

Set The App-Wide Palette Once

Use app.colors() in the composition layer for the default palette. Prefer Quasar's semantic roles over shade names: primary, secondary, accent, positive, negative, info, and warning. The dark and dark_page arguments configure dark surface colors; they do not enable dark mode.

from nicegui import app, ui

app.colors(
    primary="#176b5b",
    secondary="#52645f",
    accent="#c05a32",
    dark="#202523",
    dark_page="#151917",
    positive="#2e7d32",
    negative="#b3261e",
    info="#276b8e",
    warning="#a86600",
    brand="#176b5b",
)


@ui.page("/")
def index() -> None:
    ui.button("Save")
    ui.label("Current workspace").classes("text-brand")


ui.run()

Custom names such as brand become Quasar color names and can be used through color="brand", text-brand, or bg-brand. Register them before any component uses them. app.colors() was added in NiceGUI 3.6.0; for an older pinned version, centralize the same ui.colors(...) call in a shared page shell.

Use ui.colors() only when one page intentionally overrides the app palette. It is page-scoped and takes precedence over app.colors():

@ui.page("/operations")
def operations_page() -> None:
    ui.colors(primary="#8f3d2c")
    ui.button("Operations action")

Avoid scattering ui.colors() calls among reusable components. A component should consume semantic roles from its owning page rather than silently changing the palette for the whole page.

Let Quasar Control Light And Dark Mode

Use ui.dark_mode() for page mode. Its value is tri-state: True enables dark mode, False disables it, and None follows the client's prefers-color-scheme setting. It overrides the dark default supplied to ui.run() or @ui.page for that page.

dark_mode = ui.dark_mode(None)

with ui.button_group():
    ui.button("System", on_click=dark_mode.auto)
    ui.button("Light", on_click=dark_mode.disable)
    ui.button("Dark", on_click=dark_mode.enable)

Quasar applies body--light or body--dark, updates its dark-aware components, and tracks system changes while mode is automatic. Use the NiceGUI element instead of invoking Quasar's JavaScript Dark plugin directly. Persist an explicit user preference separately when it must survive navigation or a new browser session.

Add Semantic Tokens For Application Surfaces

Quasar's brand roles cover framework components, not every application-specific surface. Define a small set of semantic CSS variables in the static stylesheet and change their values under Quasar's documented .body--dark class:

:root {
    --app-page: #f6f8f7;
    --app-surface: #ffffff;
    --app-text: #202623;
    --app-border: #cbd4d0;
}

.body--dark {
    --app-page: var(--q-dark-page);
    --app-surface: var(--q-dark);
    --app-text: #eef3f0;
    --app-border: #46504b;
}

body {
    background: var(--app-page);
    color: var(--app-text);
}

.app-panel {
    background: var(--app-surface);
    border: 1px solid var(--app-border);
}

Name tokens by purpose, such as --app-surface or --app-muted-text, rather than by a fixed color such as --app-gray-100. Reuse --q-primary and the other Quasar variables when the meaning matches. Check text, icon, border, focus, hover, disabled, positive, warning, and negative contrast in both modes; a palette is not complete merely because the page background changes.

Structural Styling With Tailwind

Use standard Tailwind utility classes for page and component structure:

  • display, flex, and grid behavior
  • width, height, and maximum-width constraints
  • spacing, gaps, padding, and alignment
  • wrapping, overflow, and responsive variants
  • typography and common visual utilities when they fully express the design

Build the outer layout before fine-tuning individual controls:

  1. Define the page shell and width constraints.
  2. Establish responsive rows, columns, gaps, and wrapping.
  3. Add semantic sections and repeated visual patterns.
  4. Configure component appearance and behavior with constructor arguments and Quasar props.
  5. Add stable application classes for any remaining stylesheet rules.
with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4"):
    page_header(title="Inventory")

    with ui.row().classes("w-full gap-4 flex-wrap lg:flex-nowrap items-start"):
        filters_panel().classes("w-full lg:w-72 shrink-0")
        item_grid().classes("w-full flex-1 min-w-0")

Use stable width, minimum-width, and flex constraints so labels, icons, validation messages, and loaded content do not shift the surrounding layout.

Fine Tuning With Static Stylesheets

Move stable fine tuning into a static stylesheet after the structure and native component configuration are correct. Static stylesheets provide reusable selectors, media queries, pseudo-classes, CSS variables, and a clear cascade that inline declarations cannot provide.

Attach an application-owned class with .classes() or a Quasar popup prop, then scope stylesheet rules beneath it:

ui.select(...).props("popup-content-class=app-item-menu").classes(
    "app-item-select w-full md:max-w-md"
)
.app-item-select {
    --app-field-accent: #176b5b;
}

.app-item-select:focus-within {
    filter: drop-shadow(0 0 0.25rem rgb(23 107 91 / 20%));
}

.app-item-menu {
    max-height: min(24rem, 60dvh);
}

Use .style() when a value is calculated at runtime or while testing a local hypothesis. Once a declaration becomes stable or repeated, move it to the stylesheet and keep only the application class in Python.

Avoid overriding Quasar internals such as .q-field__label, .q-field__native, .q-field__control, and .q-field__input unless the public props, slots, and application-level selectors cannot express the requirement.

Quasar coordinates field height, padding, labels, values, icons, and floating-label transforms. Changing only one internal part tends to cause clipping or overlap.

Responsive Layout

Support these layouts only:

  • mobile: a single-column layout with wrapping toolbars and full-width controls
  • landscape desktop: 1920 \times 1080 with side-by-side panels where they improve scanning
  • portrait desktop: 1080 \times 1920 with stacked panels or a narrow fixed sidebar

Build the mobile layout first, then add one desktop breakpoint when a row or grid needs more space. Prefer flex wrapping and fluid grids before adding another breakpoint. Use Tailwind classes for page layout and Quasar props for component behavior.

with ui.row().classes("w-full flex-wrap gap-4 lg:flex-nowrap items-start"):
    filters_panel().classes("w-full lg:w-72 shrink-0")
    item_grid().classes("w-full flex-1 min-w-0")

Use min-w-0 for flexible children, flex-wrap for toolbars, and max-w-* mx-auto to keep portrait layouts readable. Do not add device-specific component trees, container queries, or custom breakpoints unless a supported layout demonstrates a concrete failure.

Loading Stylesheets And Static Assets

  • Mount and link static stylesheets once from the composition layer rather than injecting CSS from individual pages.
  • Keep custom CSS tokenized with variables and scoped to application classes.
  • Avoid broad rules against Quasar internals.
  • Mount referenced assets in the composition layer.
  • Verify mount paths, reverse-proxy rewrites, and cache behavior.
from pathlib import Path

from fastapi.staticfiles import StaticFiles

STATIC_DIR = Path(__file__).parent / "ui" / "static"

app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
ui.add_head_html(
    '<link rel="stylesheet" href="/static/css/base.css">',
    shared=True,
)

Worked Example: Responsive Dialog Customization

This example begins with normal field density and Quasar popup props, then uses an application class and static stylesheet for the remaining responsive fine tuning. Use whole-card scaling when a form dialog must become uniformly larger on mobile while preserving Quasar's internal proportions. Keep detached select menus unscaled and make the card itself scrollable.

Use Normal Field Density

Normal Quasar fields are approximately 56px high, while dense fields are approximately 40px high. Remove dense when larger controls are needed.

ui.input("Name").props("outlined")
ui.number("Quantity").props("outlined")
ui.select(...).props("outlined popup-content-class=app-item-detail-menu")
ui.textarea("Description").props("outlined autogrow")

Add a scoped class to the dialog card:

ui.card().classes("app-detail-card app-item-detail-card")

Scale The Complete Card

:root {
    --item-dialog-scale: 1;
    --item-dialog-max-height: calc(100dvh - 3rem);
}

.app-item-detail-card {
    width: min(50rem, 50vw);
    max-height: var(--item-dialog-max-height);
    overflow-y: auto;
    overscroll-behavior: contain;
    zoom: var(--item-dialog-scale);
}

/* Restore Quasar's baseline if a global rule overrides it. */
.app-item-detail-card .q-field,
.app-item-detail-menu {
    font-size: 14px;
}

@media (max-width: 599px) {
    :root {
        --item-dialog-scale: 1.2;
        /* 75dvh becomes 90dvh after 1.2x zoom. */
        --item-dialog-max-height: 75dvh;
    }

    .app-item-detail-card {
        width: 80vw;
    }

    .app-item-detail-menu {
        font-size: 16.8px;
    }
}

The main mobile tuning knob is:

--item-dialog-scale: 1.2;

Keep Detached Popups Unscaled

Do not apply zoom or transform: scale() to a QSelect popup menu. Quasar renders menus outside the dialog and positions them from the unscaled anchor geometry. Scaling the menu container afterward separates it from its field.

Avoid:

.app-item-detail-card,
.app-item-detail-menu {
    zoom: 1.2;
}

Use:

.app-item-detail-card {
    zoom: 1.2;
}

.app-item-detail-menu {
    font-size: 16.8px;
}

Use popup-content-class=app-item-detail-menu to target the detached menu and enlarge its text without changing its coordinate system.

Account For Zoom When Scrolling

The card's pre-zoom maximum height must account for the scale:

[ \begin{aligned} h_{\mathrm{pre}} &= \frac{h_{\mathrm{visible}}}{s} \ \text{where } s &= \text{the zoom scale} \end{aligned} ]

For a desired visual height of 90dvh at (1.2\times):

[ \frac{90,\mathrm{dvh}}{1.2} = 75,\mathrm{dvh} ]

Therefore:

--item-dialog-max-height: 75dvh;

Apply scrolling to the card itself:

.app-item-detail-card {
    max-height: var(--item-dialog-max-height);
    overflow-y: auto;
    overscroll-behavior: contain;
}

This keeps the dimmed page stationary while the form scrolls.

Match The Quasar Breakpoint

Quasar's extra-small breakpoint ends at 599.98px. A mobile-only rule can use:

@media (max-width: 599px) {
    /* Mobile rules. */
}

Confirm custom breakpoint values against the target application's Quasar configuration.

Validation Checklist

Check each completed page at these three viewports:

  1. A representative mobile viewport, such as 390 \times 844.
  2. Landscape desktop at 1920 \times 1080.
  3. Portrait desktop at 1080 \times 1920.

Confirm that page sections do not overlap, toolbars wrap on mobile, desktop panels use the available space without becoming excessively wide, and dialogs remain visible and scroll to their final field.

Sources

!!! info "Primary sources" - NiceGUI element styling and props - NiceGUI binding properties - NiceGUI color theming - NiceGUI dark mode - Quasar components - Quasar color palette and runtime brand variables - Quasar dark mode - Quasar field - Quasar select - Tailwind responsive design - MDN zoom