# 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: ```text 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: 1. Define the page shell and width constraints. 2. Establish responsive rows, columns, gaps, and wrapping. 3. Add semantic sections and repeated components. 4. Configure Quasar component appearance with props. 5. Add custom CSS only for behavior that props and utilities cannot express safely. ```python 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. ```python 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: 1. Use Quasar props for component appearance, density, labels, and popup behavior. 2. Use NiceGUI `.classes()` and Tailwind utilities for width, spacing, alignment, and responsive layout. 3. Use reusable component functions for repeated visual patterns. 4. Use `.style()` for genuinely dynamic inline values. 5. Use minimal shared CSS only when props and utilities are insufficient. Common Quasar props include: - `outlined` - `dense` - `stack-label` - `popup-content-class` - `input-class` - `input-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: ```python 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-0` on 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. ```python 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. ```python 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: ```python ui.card().classes("app-detail-card app-item-detail-card") ``` ### Scale The Complete Card ```css :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: ```css --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: ```css .app-item-detail-card, .app-item-detail-menu { zoom: 1.2; } ``` Use: ```css .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: ```css --item-dialog-max-height: 75dvh; ``` Apply scrolling to the card itself: ```css .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: ```css @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 > clientHeight` when 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](https://nicegui.io/documentation/element) - [NiceGUI binding properties](https://nicegui.io/documentation/section_binding_properties) - [Quasar components](https://quasar.dev/vue-components) - [Quasar field](https://quasar.dev/vue-components/field/) - [Quasar select](https://quasar.dev/vue-components/select/) - [Quasar breakpoints](https://quasar.dev/style/breakpoints/) - [Tailwind responsive design](https://tailwindcss.com/docs/responsive-design) - [MDN `zoom`](https://developer.mozilla.org/en-US/docs/Web/CSS/zoom) - [Playwright input actions](https://playwright.dev/docs/input)