9.3 KiB
NiceGUI Page Layout And Styling
Use this reference to structure NiceGUI pages, choose component boundaries, apply responsive layout, and introduce custom CSS without fighting Quasar's internal geometry.
Ownership And Dependency Boundaries
Keep dependencies flowing in one direction:
- pages import components and services
- components contain presentation logic only
- services contain business logic and do not import UI
- bootstrap code mounts static assets and loads shared CSS once
Suggested module split:
src/my_app/
ui/
pages/
components/
static/
services/
api/
Page modules should compose a route from reusable presentation and service calls. They should not own domain rules, persistence, or long-running synchronous work.
Page Composition
Build the outer layout before styling individual controls:
- Define the page shell and width constraints.
- Establish responsive rows, columns, gaps, and wrapping.
- Add semantic sections and repeated components.
- Configure Quasar component appearance with props.
- Add custom CSS only for behavior that props and utilities cannot express safely.
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.
Component Extraction
Extract a presentation pattern to ui/components/ when it appears on two or more pages or when it owns a meaningful interaction boundary. Keep one-off route layout in the page module.
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
Reusable components should accept data and event callbacks rather than import page state or business services implicitly.
Styling Decision Order
NiceGUI wraps Quasar components. Choose the styling mechanism according to what it owns:
- Use Quasar props for component appearance, density, labels, and popup behavior.
- Use NiceGUI
.classes()and Tailwind utilities for width, spacing, alignment, and responsive layout. - Use reusable component functions for repeated visual patterns.
- Use
.style()for genuinely dynamic inline values. - Use minimal shared CSS only when props and utilities are insufficient.
Common Quasar props include:
outlineddensestack-labelpopup-content-classinput-classinput-style
Avoid overriding internal selectors such as:
.q-field__label.q-field__native.q-field__control.q-field__input
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
Use Tailwind breakpoint classes for ordinary page adaptation:
with ui.row().classes("w-full gap-4 flex-wrap sm:flex-nowrap"):
ui.card().classes("w-full sm:flex-1 sm:min-w-64")
ui.card().classes("w-full sm:flex-1 sm:min-w-64")
- Start with a usable mobile layout, then add larger breakpoint behavior.
- Allow dense toolbars to wrap or collapse intentionally.
- Use
min-w-0on flexible content that must shrink inside a row. - Keep controls and primary actions visible without horizontal scrolling.
- Test the longest realistic labels, values, errors, and menu options.
Static Assets And Shared CSS
- Mount static assets from the composition layer.
- Load shared CSS once rather than injecting it from individual pages.
- Keep custom CSS tokenized with variables and scoped to application classes.
- Avoid broad rules against Quasar internals.
- 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_css((STATIC_DIR / "css" / "base.css").read_text(encoding="utf-8"))
Responsive Dialog Pattern
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:
ext{pre-zoom max height}
=
\frac{\text{desired visible height}}{\text{scale}}
For a desired visual height of 90dvh at 1.2\times:
90 / 1.2 = 75
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
Test the complete page at representative mobile and desktop viewports. For a dialog, include a mobile viewport such as 390 \times 844.
- Page sections do not overlap or introduce unintended horizontal scrolling.
- Responsive rows wrap or resize as designed.
- Dialog remains inside the viewport.
- Dialog has
scrollHeight > clientHeightwhen its content is taller than its maximum height. - Scrolling reaches the final form field.
- Select menus open directly against their fields.
- Menus have no horizontal overflow.
- Values and floating labels are not clipped.
- Select arrows and other icons scale with the card.
- Detached popup menus report
zoom: 1.
As a precision check, compare the menu edge with the field edge using browser geometry. One corrected implementation measured within approximately 0.5px horizontally and 0.14px vertically; treat those values as an example observation, not a framework guarantee.
Playwright Caveat
Playwright locator clicks can calculate incorrect coordinates for elements inside CSS zoom. A failed locator click does not necessarily mean browser interaction is broken.
For verification, either:
- click using manually adjusted visual coordinates
- trigger the element through DOM evaluation
- test the interaction manually in a real browser
Do not alter otherwise correct component styling solely to accommodate this automation limitation.
Sources
!!! info "Primary sources"
- NiceGUI element styling and props
- NiceGUI binding properties
- Quasar components
- Quasar field
- Quasar select
- Quasar breakpoints
- Tailwind responsive design
- MDN zoom
- Playwright input actions