8.6 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
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 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.
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:
[ \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
- Quasar components
- Quasar field
- Quasar select
- Tailwind responsive design
- MDN zoom