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:
- Read the NiceGUI documentation page for the component.
- Inspect the NiceGUI element function or class constructor.
- Identify the wrapped Quasar component and read its documentation.
- Use Quasar props, slots, and events through NiceGUI's native customization APIs.
- Use Tailwind classes for structural layout.
- 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:
- Configure Quasar's named color roles through NiceGUI.
- Let Quasar own light, dark, and automatic mode state.
- 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:
- Define the page shell and width constraints.
- Establish responsive rows, columns, gaps, and wrapping.
- Add semantic sections and repeated visual patterns.
- Configure component appearance and behavior with constructor arguments and Quasar props.
- 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 1080with side-by-side panels where they improve scanning - portrait desktop:
1080 \times 1920with 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:
- A representative mobile viewport, such as
390 \times 844. - Landscape desktop at
1920 \times 1080. - 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