Compare commits

..
2 Commits
Author SHA1 Message Date
John Lancaster f6752313be expanded other pages 2026-08-30 09:19:54 -05:00
John Lancaster 12f916455b nicegui styling 2026-08-30 01:22:03 -05:00
6 changed files with 873 additions and 433 deletions
+40 -142
View File
@@ -1,164 +1,62 @@
---
name: nicegui
description: 'Reference hub for NiceGUI and FastAPI application structure, typed configuration, ASGI and Uvicorn startup, UI composition, styling, bindable state, interactions, troubleshooting, testing, and source documentation. Use when planning, implementing, reviewing, deploying, or debugging NiceGUI applications; load only the references relevant to the task.'
description: 'Build, review, and debug NiceGUI applications. Use for FastAPI or Uvicorn integration, app factories and lifespan, ui.* components, Quasar props/events/slots, Tailwind page layout, colors and dark mode, bindings and bindable_dataclass, editable ui.table cells, uploads/forms/live updates, or version-specific NiceGUI source research.'
---
# NiceGUI Reference
# NiceGUI Application Guide
Use this skill as a progressive reference for NiceGUI applications built with FastAPI. Start with the routing map, load only the material needed for the current question, and reconcile it with the target project's NiceGUI version and established conventions.
Use this skill to choose the smallest supporting reference for a NiceGUI task. The pages cover different ownership boundaries; do not load the whole reference set.
## When to Use
## Workflow
- Planning or reviewing NiceGUI application structure and FastAPI composition.
- Building or refactoring pages, components, layouts, and static assets.
- Creating editable tables with Python-authoritative state, validation, and persistence.
- Modeling UI state with bindings or bindable dataclasses.
- Implementing forms, uploads, refreshes, live updates, or background work.
- Diagnosing UI state, concurrency, navigation, or asset problems.
- Verifying framework behavior against primary documentation.
1. Inspect the target project's pinned NiceGUI version, entry point, and existing page/component patterns.
2. Match the request to one row in the routing table and load that primary reference.
3. Load the optional companion only when the task crosses the boundary named in the last column.
4. Prefer NiceGUI's typed constructor, binding, or helper API; descend to Quasar props, events, slots, or methods only when the wrapper does not expose the required behavior.
5. Validate the changed behavior with a focused test. For visual work, also check the supported mobile, landscape desktop, and portrait desktop viewports.
## How to Use This Skill
## Task Routing
1. Classify the request using the discovery map below.
2. Load the smallest relevant reference, or at most two references for a mixed concern.
3. Inspect the target repository before applying guidance; preserve its sound local patterns.
4. Check the pinned NiceGUI and integration versions before relying on version-specific APIs.
5. Validate the changed behavior with focused tests and, for UI work, relevant viewport checks.
| Task or symptom | Load first | Add only when |
| --- | --- | --- |
| Choose package boundaries, dependency direction, page registration, health routes, or optional persistence, LangGraph, and mounted-docs placement | [application architecture](./references/architecture.md) | Add [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) for concrete ASGI ownership or startup code. |
| Decide between `ui.run()` and `ui.run_with()`, compose a parent FastAPI app, define lifespan ordering, build an app factory, configure typed settings, expose a project script, or handle reload/workers | [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) | Add [application architecture](./references/architecture.md) only for wider package placement. |
| Choose a `ui.*` constructor, binding, Quasar prop, event, slot, or frontend method; diagnose model events, event payloads, scoped-slot props, detached popups, `ui.select`, or `ui.icon` | [component mechanics](./references/component-mechanics.md) | Add [source documentation](./references/source-documentation.md) when the installed wrapper or bundled Quasar version must be verified. |
| Build page shells, rows, columns, grids, widths, overflow, responsive reflow, typography, font loading, static assets, or deliberate scaling | [page structure, typography, and scaling](./references/styling-and-customization.md) | Add [component mechanics](./references/component-mechanics.md) when layout depends on a Quasar prop, slot, popup, or generated component structure. |
| Configure `app.colors()`, `ui.colors()`, semantic or fixed Quasar colors, custom color names, component color values, CSS color variables, or `ui.dark_mode()` | [NiceGUI and Quasar color theming](./references/colors-and-quasar-theming.md) | Add [page structure, typography, and scaling](./references/styling-and-customization.md) only when the task also changes physical layout or CSS loading. |
| Model typed page state with `binding.bindable_dataclass`, understand propagation and transform direction, bind nested values, avoid active-link polling, or design projection/persistence rollback | [binding dataclasses](./references/binding-dataclasses.md) | Add [component mechanics](./references/component-mechanics.md) for browser-originated event proposals. |
| Make `ui.table` cells editable with stable row keys, dataframe projections, row-scoped dataclasses, validation, touched rows, selection-preserving refresh, or `QPopupEdit` | [editable tables](./references/tables.md) | Follow its links to binding or component mechanics only when changing the underlying projection or event bridge. |
| Implement uploads, form submission, SSE versus WebSockets, background jobs, duplicate-submit guards, or explicit `@ui.refreshable` refreshes | [interaction patterns](./references/interaction-patterns.md) | Add [binding dataclasses](./references/binding-dataclasses.md) when state propagation itself is the problem. |
| Investigate upload errors, async UI races, stale assets, navigation/state drift, or perform a compact production-readiness review | [troubleshooting and quality gates](./references/troubleshooting-and-quality-gates.md) | Follow the symptom to one detailed reference above. |
| Verify a framework claim against primary NiceGUI, FastAPI, Uvicorn, Tailwind, Quasar, SQLAlchemy, Pydantic, or LangGraph documentation | [source documentation](./references/source-documentation.md) | Use a task page first when implementation guidance, not source lookup, is needed. |
## Progressive Discovery Map
## Boundary Rules
### Application Architecture
- Use [application architecture](./references/architecture.md) for module ownership, not for page geometry or low-level component behavior.
- Use [page structure, typography, and scaling](./references/styling-and-customization.md) for physical layout. Use [component mechanics](./references/component-mechanics.md) for the behavior crossing NiceGUI, Quasar, Vue, and browser boundaries.
- Use [binding dataclasses](./references/binding-dataclasses.md) for the binding graph and Python model projections. Use [interaction patterns](./references/interaction-patterns.md) for user workflows such as upload, submit, refresh, streaming, and background work.
- Start editable-table work in [editable tables](./references/tables.md). It already identifies the exact binding and event sections needed by that pattern.
- Treat [source documentation](./references/source-documentation.md) as a source index, not as an implementation workflow.
Load [application architecture](./references/architecture.md) for:
## Runnable Examples
- FastAPI app factories and lifespan ownership
- package boundaries and dependency direction
- page registration and health routes
- optional persistence, LangGraph, or mounted documentation
- async responsiveness and baseline tests
Load an example only when its exact mechanic matches the task:
### FastAPI And Uvicorn Startup
- [binding transforms](./examples/data_binding.py): `bindable_dataclass`, `ui.date`, and typed `forward`/`backward` conversion.
- [select events](./examples/select_events.py): `on_change`, generic Quasar events, `update:model-value`, browser-to-Python payload forwarding, and programmatic value changes.
- [editable table](./examples/editable_table.py): dataframe-to-row state, named QTable cell slots, controlled editors, Python validation, touched rows, and canonical row refresh.
Load [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) for:
- choosing between `ui.run()` and `ui.run_with()`
- understanding the parent FastAPI app and NiceGUI's internal app
- composing ASGI lifespan and mounted routes
- loading one typed settings snapshot for server and application configuration
- serving an app instance or factory with Uvicorn
- exposing programmatic startup through `[project.scripts]`
- reload, worker, and process-local state constraints
### Styling And Customization
Load [styling and customization](./references/styling-and-customization.md) for:
- app-wide and page-level color themes, dark mode, and semantic CSS tokens
- Tailwind and Quasar utility classes
- scoped CSS properties and stable application classes
- responsive page composition and static asset loading
- cosmetic treatment of controls, surfaces, typography, and visual states
- visual validation at supported viewport sizes
### Component Mechanics
Load [component mechanics](./references/component-mechanics.md) for:
- the NiceGUI Python wrapper, element bridge, Quasar component, and Vue runtime boundaries
- deciding between constructors, bindings, Quasar props, events, slots, and frontend methods
- controlled values, model events, transformed payloads, and server-authoritative edit proposals
- server-client state and event flow, validation timing, and commit policy
- detached content and external icon assets
- source research against the installed NiceGUI and bundled Quasar versions
- `ui.select` and `ui.icon` mechanics and caveats
- scoped component slots and their interaction contracts
### Editable Tables
Load [editable tables](./references/tables.md) for:
- Python-authoritative editable `ui.table` state
- rendering dataframe records into row-scoped bindable dataclasses
- stable row identity across sorting, filtering, and pagination
- NiceGUI editors in Quasar `body-cell-*` scoped slots
- QTable row refresh and selection preservation after edits
- the full `body` slot required when escalating to `QPopupEdit`
### Bindable State
Load [bindable dataclasses](./references/binding-dataclasses.md) for:
- typed local UI state
- propagation, serializable projections, persistence, and rollback behavior
- nested structures and strict bindings
- mutable defaults, performance, and version notes
### Interaction Patterns
Load [interaction patterns](./references/interaction-patterns.md) for:
- uploads and form submission
- explicit refreshes
- server-sent events and WebSockets
- background work and duplicate-submission guards
### Troubleshooting And Quality
Load [troubleshooting and quality gates](./references/troubleshooting-and-quality-gates.md) for:
- upload failures and UI race conditions
- stale assets and navigation drift
- responsiveness, accessibility, reliability, and maintainability checks
### Primary Sources
Load [source documentation](./references/source-documentation.md) when:
- behavior is version-sensitive or uncertain
- an integration recommendation needs verification
- upstream NiceGUI, FastAPI, Tailwind, Quasar, SQLAlchemy, Pydantic, or LangGraph documentation is required
## Common Discovery Paths
### New Application Or Architecture Review
1. Load [application architecture](./references/architecture.md).
2. Add [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) when FastAPI owns the application or startup must be exposed as a project command.
3. Add [styling and customization](./references/styling-and-customization.md) only when page layout or visual customization is in scope.
### Page Or Component Work
1. Load [application architecture](./references/architecture.md) for page and component ownership decisions.
2. Load [styling and customization](./references/styling-and-customization.md) for themes, layout, responsive presentation, utility classes, or CSS.
3. Load [component mechanics](./references/component-mechanics.md) when behavior must be mapped across NiceGUI, Quasar, and Vue, or when detached content and component-specific behavior are involved.
4. Load [editable tables](./references/tables.md) when table cells accept user changes or `QPopupEdit` is being considered.
5. Add [interaction patterns](./references/interaction-patterns.md) or [bindable dataclasses](./references/binding-dataclasses.md) according to the page behavior.
### Debugging Or Production Review
1. Start with [troubleshooting and quality gates](./references/troubleshooting-and-quality-gates.md).
2. Follow the symptom to one detailed reference.
3. Confirm uncertain behavior in [source documentation](./references/source-documentation.md).
## General Defaults
## Defaults That Span References
- Keep composition, transport, services, pages, and components directionally separated.
- Keep business logic out of UI components and event handlers.
- Avoid blocking I/O and CPU-heavy work in the UI event loop.
- Prefer event-driven updates and explicit refreshes over unrelated polling.
- Discover component capabilities through NiceGUI docs and constructors, then the wrapped Quasar API.
- Keep editable table records authoritative in Python; send stable row keys with edit proposals and reassert canonical rows after validation.
- Research the current NiceGUI and Quasar source documentation before generating component-specific code or CSS.
- Prefer constructor arguments and native Quasar features through NiceGUI; use Tailwind for structure and scoped static CSS for stable fine tuning.
- Prefer event-driven updates and explicit refreshes to unrelated polling.
- Keep browser-originated values as proposals; validate and normalize them in Python before mutating authoritative state.
- Use Tailwind for physical structure and scoped static CSS only for requirements that NiceGUI, Quasar, or utilities cannot express cleanly.
- Provide loading, success, and failure states for user-triggered work.
- Treat version-specific guidance as a prompt to verify the project's dependency version.
- Verify component-specific behavior against the installed NiceGUI version and its bundled Quasar version.
## Reference Use Contract
## Completion Check
When applying this skill:
- return only guidance relevant to the current task
- distinguish repository facts from reference recommendations
- cite the appropriate source reference for framework-level claims
- state assumptions when application requirements are missing
- report the focused checks used to validate implementation changes
Before finishing, distinguish target-repository facts from reference recommendations, cite the supporting page used for framework-specific claims, state unresolved assumptions, and report the focused behavior and viewport checks performed.
@@ -0,0 +1,189 @@
# NiceGUI And Quasar Color Theming
This reference describes how NiceGUI's Python color APIs map onto Quasar's browser-side color system. It distinguishes theme configuration from individual element colors, fixed palette colors from runtime brand roles, and palette values from dark-mode state.
The primary public references are [NiceGUI styling and appearance](https://nicegui.io/documentation/section_styling_appearance), [NiceGUI color theming](https://nicegui.io/documentation/colors), and the [Quasar color palette](https://quasar.dev/style/color-palette).
## Boundary At A Glance
NiceGUI does not define an independent component theme engine. It configures and consumes the Quasar color system while adding Python-facing scope, value classification, and CSS cascade behavior.
| Surface | NiceGUI owns | Quasar or the browser owns |
| --- | --- | --- |
| `app.colors(...)` | application-wide Python configuration and custom-name registration | initial Quasar brand configuration and the resulting `--q-*` values on each page |
| `ui.colors(...)` | a page-level element and precedence over `app.colors()` | runtime `--q-*` properties on `document.body` plus custom `text-*` and `bg-*` classes |
| component `color=` and `text_color=` arguments | classification of supported values as Quasar, Tailwind, or CSS colors on color-aware wrappers | rendering through a Quasar prop, a utility class, or an inline CSS declaration |
| `.props("color=...")` | transport of the prop to the frontend component | interpretation of the value by that Quasar component |
| `.classes("text-primary bg-positive")` | attachment of class names and NiceGUI's CSS layer arrangement | Quasar's semantic utility classes and their `--q-*` variable references |
| `ui.dark_mode(...)` | Python control and binding with `True`, `False`, or automatic `None` state | Quasar dark-mode state, `body--light` or `body--dark`, and dark-aware components |
The central handoff is a CSS custom property. NiceGUI supplies a value such as `#176b5b`; Quasar components and helpers consume `var(--q-primary)`.
## Quasar Color Namespaces
Quasar exposes two materially different kinds of color name. Only one kind is changed by NiceGUI's theme APIs.
### Runtime Brand Roles
Quasar's semantic brand roles are backed by root or body-level CSS custom properties. Components and semantic utility classes follow these values at runtime. NiceGUI exposes the eight Quasar brand roles and the separate dark-page surface through `app.colors()` and `ui.colors()`.
| NiceGUI argument | CSS custom property | NiceGUI default | Intended meaning |
| --- | --- | --- | --- |
| `primary` | `--q-primary` | `#5898d4` | main action and brand emphasis |
| `secondary` | `--q-secondary` | `#26a69a` | secondary brand emphasis |
| `accent` | `--q-accent` | `#9c27b0` | accent emphasis |
| `dark` | `--q-dark` | `#1d1d1d` | dark component surface |
| `dark_page` | `--q-dark-page` | `#121212` | dark page background |
| `positive` | `--q-positive` | `#21ba45` | success state |
| `negative` | `--q-negative` | `#c10015` | error or destructive state |
| `info` | `--q-info` | `#31ccec` | informational state |
| `warning` | `--q-warning` | `#f2c037` | warning state |
For example, `color="primary"`, `.props("color=primary")`, `text-primary`, and `bg-primary` all reach Quasar's semantic primary role. Changing that role changes every consumer of `--q-primary`; it does not rewrite fixed palette colors.
```python
from nicegui import app, ui
app.colors(
primary="#176b5b",
secondary="#52645f",
accent="#c05a32",
positive="#2e7d32",
negative="#b3261e",
info="#276b8e",
warning="#a86600",
dark="#202523",
dark_page="#151917",
)
ui.button("Save")
ui.label("Saved").classes("text-positive")
```
The current NiceGUI client implementation writes page-level values to `document.body` in [`colors.js`](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/colors.js). Quasar's semantic helpers reference those properties, as described under [dynamic brand colors](https://quasar.dev/style/color-palette#dynamic-change-of-brand-colors-dynamic-theme-colors).
### Fixed Palette Colors
Names such as `red-5`, `teal-10`, and `blue-grey-2` belong to Quasar's compiled [color list](https://quasar.dev/style/color-palette#color-list). Their `text-*` and `bg-*` classes contain fixed color values rather than references to the semantic brand variables.
Consequently:
- `ui.colors(primary="#0057b8")` changes `primary`, `text-primary`, and `bg-primary` consumers.
- It does not change `blue`, `blue-6`, `text-blue-6`, or `bg-blue-6`.
- A fixed palette color can be assigned to a component, for example `ui.button("Open", color="teal-7")`, without adding it to the application theme.
The fixed palette is a Quasar facility bundled into NiceGUI. It is not generated by `app.colors()` or `ui.colors()`.
### Custom Semantic Names
Extra keyword arguments create application-specific names:
```python
from nicegui import app, ui
app.colors(brand="#176b5b", review_required="#a86600")
ui.button("Continue", color="brand")
ui.label("Review required").classes("text-review-required")
```
NiceGUI normalizes underscores in Python keyword names to hyphens in browser color names. For each custom name, the client-side [`applyColors`](https://github.com/zauberzeug/nicegui/blob/main/nicegui/static/nicegui.js) helper creates:
- a `--q-<name>` property on `document.body`
- a `.text-<name>` class that reads that property
- a `.bg-<name>` class that reads that property
This automates the custom-class pattern shown in Quasar's [adding your own colors](https://quasar.dev/style/color-palette#adding-your-own-colors) reference. NiceGUI also registers the name in its Python-side Quasar color set so color-aware wrappers pass the value as a Quasar color prop. The name must therefore be declared with `app.colors()` or `ui.colors()` before a NiceGUI component first uses it; this ordering requirement is part of the [NiceGUI custom colors contract](https://nicegui.io/documentation/colors#custom_colors).
## Scope And Precedence
The effective palette has three levels:
| Level | Scope | Effect |
| --- | --- | --- |
| bundled Quasar values | every page | fallback values supplied by Quasar's CSS |
| `app.colors(...)` | all NiceGUI pages | populates NiceGUI's Quasar brand configuration before each client app starts |
| `ui.colors(...)` | current page | writes the core and custom properties on that page's `document.body` and takes precedence over app-wide values |
`app.colors()` is configuration, not a rendered UI element. NiceGUI stores its values in the application's Quasar configuration; see the current [`App.colors` implementation](https://github.com/zauberzeug/nicegui/blob/main/nicegui/app/app.py).
`ui.colors()` is rendered into a specific page. Its DOM placement in a row, card, or other container does not scope the palette to that subtree because its client component writes to `document.body`. A page with two calls therefore has one effective page palette, with the last mounted call determining the core values. Subtree-specific theming requires application CSS variables or directly scoped `--q-*` overrides, not nested `ui.colors()` elements.
The `ui.colors()` initializer supplies all nine core values. A call such as `ui.colors(primary="#555")` is therefore a complete core-palette assignment: unspecified roles resolve to NiceGUI's defaults rather than acting as a one-property patch over `app.colors()`. Pages that must retain customized app-wide secondary, status, or dark values should pass those values explicitly in the page override.
`app.colors()` was added in NiceGUI 3.6.0, while custom colors were added to `ui.colors()` in 2.2.0. Applications pinned to earlier NiceGUI releases need version-matched behavior from the [NiceGUI colors reference](https://nicegui.io/documentation/colors).
## Element Color Values
On elements implemented with NiceGUI's color mixins, a `color`, `text_color`, or corresponding setter value is classified in this order by [`color_elements.py`](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/mixins/color_elements.py):
| Input kind | Example | NiceGUI output | Theme response |
| --- | --- | --- | --- |
| Quasar semantic, fixed, or registered custom name | `primary`, `red-5`, `brand` | Quasar component color prop | semantic and custom names follow `--q-*`; fixed names do not |
| recognized Tailwind color | `red-500` | `bg-red-500` or `text-red-500` class | independent of the Quasar palette |
| other CSS color value | `#ff0000`, `rgb(255 0 0)`, `rebeccapurple` | inline `background-color` or `color` | independent of the Quasar palette |
| `None` | `None` | removes the managed color | falls back to component and cascade defaults |
This classification is a NiceGUI convenience, not a general Quasar rule. Passing `.props("color=#ff0000")` bypasses NiceGUI's color mixin and asks the Quasar component to interpret `#ff0000` as its `color` prop. Likewise, components that expose a raw Quasar color prop without using the mixin may accept only the values documented by that component. The specific NiceGUI constructor documentation remains authoritative for each element.
Quasar and Tailwind color classes share the same HTML class list but not the same namespace conventions. `text-red-5` is a Quasar fixed-palette helper; `text-red-500` is a Tailwind-compatible utility. Semantic names such as `text-primary` are Quasar helpers.
## Palette Values And Dark Mode Are Separate
The `dark` and `dark_page` arguments define colors; they do not enable dark mode. Mode state is controlled by [`ui.dark_mode()`](https://nicegui.io/documentation/dark_mode), the `dark` argument of `ui.run()`, or a page decorator. `ui.dark_mode()` takes precedence for its page and maps `None` to Quasar's automatic system-preference mode.
When dark mode is active, Quasar:
- applies `body--dark` instead of `body--light`
- uses the dark page background and dark-aware component behavior
- automatically enables the dark state of Quasar components that support a `dark` prop
These behaviors are defined by [Quasar dark mode](https://quasar.dev/style/dark-mode). Application-owned surfaces can key off the same body class and reuse Quasar variables:
```css
:root {
--app-surface: #ffffff;
--app-text: #202623;
}
.body--dark {
--app-surface: var(--q-dark);
--app-text: #eef3f0;
}
```
Changing `--q-dark` while the page remains in light mode changes consumers of the `dark` role but does not add `body--dark`. Enabling dark mode without designing application-specific text, border, and surface tokens does not automatically recolor arbitrary custom CSS.
## CSS Classes And Cascade
NiceGUI ships Quasar's color helpers, so `.classes("text-primary")` and `.classes("bg-warning")` can be attached directly to NiceGUI elements. Quasar defines these helpers with `!important`.
NiceGUI changes the cascade arrangement around the bundled Quasar CSS. Its [CSS layer reference](https://nicegui.io/documentation/section_styling_appearance#css_layers) explains how Quasar rules are split into layers so important Tailwind utilities or application rules in suitable layers can override them. This is a NiceGUI integration detail; the class names and color semantics still come from Quasar.
Direct CSS can consume the same semantic properties without a Quasar class:
```css
.app-focus-ring {
outline: 2px solid var(--q-primary);
}
```
Such CSS follows runtime palette changes because it reads the same property. A literal declaration such as `outline-color: #176b5b` does not.
## Source Index
!!! info "Primary sources"
- [NiceGUI styling and appearance](https://nicegui.io/documentation/section_styling_appearance)
- [NiceGUI color theming](https://nicegui.io/documentation/colors)
- [NiceGUI dark mode](https://nicegui.io/documentation/dark_mode)
- [Quasar color palette](https://quasar.dev/style/color-palette)
- [Quasar dark mode](https://quasar.dev/style/dark-mode)
- [Quasar theme builder](https://quasar.dev/style/theme-builder)
!!! info "Implementation references"
- [NiceGUI app-wide color configuration](https://github.com/zauberzeug/nicegui/blob/main/nicegui/app/app.py)
- [NiceGUI page color element](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/colors.py)
- [NiceGUI page color client component](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/colors.js)
- [NiceGUI custom color CSS generation](https://github.com/zauberzeug/nicegui/blob/main/nicegui/static/nicegui.js)
- [NiceGUI element color classification](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/mixins/color_elements.py)
- [NiceGUI color behavior tests](https://github.com/zauberzeug/nicegui/blob/main/tests/test_colors.py)
@@ -2,7 +2,7 @@
NiceGUI components are Python objects that describe browser UI elements. A component constructor creates an element, constructor arguments configure its common behavior, and methods on the returned object expose styling, events, bindings, slots, and client-side capabilities.
This reference begins with those everyday component APIs, then describes the NiceGUI, Quasar, Vue, and browser layers beneath them. Themes, responsive composition, and broader visual design are covered separately in [visual styling and CSS](./styling-and-customization.md).
This reference begins with those everyday component APIs, then describes the NiceGUI, Quasar, Vue, and browser layers beneath them. Page structure, typography, responsive composition, and scaling are covered separately in [styling and customization](./styling-and-customization.md).
## Basic Components
@@ -1,110 +1,265 @@
# Interaction Patterns Reference
# NiceGUI Interaction Mechanics
## Reactive State
Use this reference for user-driven and live application behavior: page and client lifetime, value validation, form submission, uploads, explicit refreshes, timers, application events, background execution, and server-pushed updates. Component-specific event names, scoped event payloads, and Quasar model contracts are covered in [component mechanics](./component-mechanics.md). Binding graph behavior and typed projections are covered in [binding dataclasses](./binding-dataclasses.md).
Use bindable dataclasses for local page state.
The NiceGUI implementation details below are verified against NiceGUI `3.16.0`. FastAPI's native `EventSourceResponse` and `ServerSentEvent` APIs require FastAPI `0.135.0` or later. Check the target application's pinned versions before depending on those surfaces.
## Interaction Boundary Map
| Boundary | Owns | Does not own |
| --- | --- | --- |
| NiceGUI element | browser-facing value, enabled state, validation display, and registered UI callbacks | domain authorization, durable persistence, or cross-worker coordination |
| Page `Client` | one page visit's elements, UI context, socket connection, outbox, and client-scoped storage | durable user identity or shared application state |
| Page state | current filters, drafts, selections, busy flags, and serializable projections | database transactions or durable job state |
| Service or repository | domain validation, authorization, transactions, idempotency, and persistence | direct creation or mutation of NiceGUI elements |
| NiceGUI task utility | scheduling work in the event loop, a thread, or a process | durable delivery after process failure |
| FastAPI route | HTTP, SSE, or custom WebSocket protocol and authentication boundary | automatic synchronization with NiceGUI elements |
NiceGUI already uses a Socket.IO connection to carry element events and server updates for each client. Ordinary page interactions should use component callbacks, bindings, `Event`, and element updates rather than introducing a second transport.
## Page And Client Lifetime
A [`@ui.page`](https://nicegui.io/documentation/page) builder creates a private `Client` and element tree for each page visit. During initial page construction, Python can create elements before the browser socket exists. Code that requires JavaScript, tab storage, or post-response work must first await `ui.context.client.connected()`.
The tagged [`page` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/page.py) distinguishes two phases:
1. Before connection, the page builder must produce the initial response within `response_timeout`, which defaults to three seconds.
2. Once `connected()` is awaited, NiceGUI can send the initial HTML immediately and let the remaining async builder continue with a live client.
Long service calls should not delay initial page construction. Render a stable loading state, await the connection where necessary, then perform the asynchronous work and update or refresh the bounded result region.
### Disconnect, Reconnect, And Delete
The tagged [`Client` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/client.py) treats a transient socket disconnect differently from client deletion:
- `on_disconnect` runs whenever the socket disconnects, including interruptions followed by reconnection.
- NiceGUI keeps the client alive for the page's `reconnect_timeout`.
- A successful handshake within that window cancels pending deletion.
- `on_delete` runs only when the client is actually removed after the reconnect window or explicit cleanup.
- Deletion removes the client's elements and bindings and stops its outbox.
Use `on_disconnect` for connection telemetry and reversible transport state. Use `on_delete` to release resources owned by the page visit. Do not close a page-owned resource on every disconnect if it must survive a short reconnect.
NiceGUI's tagged [`Outbox`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/outbox.py) retains recent messages according to `message_history_length` and the reconnect window. A reconnecting client supplies its next expected message ID; NiceGUI replays retained messages or reloads the page when the required history is unavailable. Message replay is transport recovery, not a durable event log or a substitute for idempotent service operations.
### State Scope
[`app.storage`](https://nicegui.io/documentation/storage) offers scopes with different navigation and process lifetimes:
| Scope | Shared with | Survives page navigation or reload | Persistence notes |
| --- | --- | --- | --- |
| `client` | current page visit only | no | server memory; appropriate for short-lived page resources |
| `tab` | current browser tab | yes | server memory by default; requires an established connection |
| `user` | tabs carrying the same signed session ID | yes | server-side persistent dictionary; requires `storage_secret` |
| `browser` | tabs sharing the session cookie | yes | cookie payload; writable only before the response is built; prefer `user` for most data |
| `general` | all users in the process or configured backend | yes | shared persistent dictionary; not a per-user boundary |
The tagged [`storage` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/storage.py) stores general and user data in local JSON files by default or Redis when configured. Tab storage is process memory unless Redis is configured. Multiple workers therefore require an explicitly shared backend for state that must cross processes.
## Values, Validation, And Submission
NiceGUI value elements mirror browser changes into Python and then invoke `on_change` or `on_value_change` handlers. For text input, [`ui.input`](https://nicegui.io/documentation/input) sends `on_change` on each value change unless a Quasar `debounce` prop delays the model update. Use an enter, blur, or explicit submit event when every keystroke should not trigger application work.
The tagged [`ValidationElement`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/mixins/validation_element.py) implements NiceGUI's Python validation:
- a callable returns an error string or `None`
- a dictionary maps error strings to predicates and stops at the first failed predicate
- automatic validation runs after each handled value change unless `without_auto_validation()` is set
- `validate()` updates the element's `error` and `error-message` props
- asynchronous validation runs as a background task; `validate(return_result=True)` is not supported for an async validator
NiceGUI validation is suitable for field feedback, but a submit operation still needs service-level validation and authorization. Browser values, client-side Quasar rules, file metadata, and hidden or disabled controls are not trust boundaries.
NiceGUI does not require a transport-level HTML form for ordinary page submission: current element values already exist in Python. A submit handler can validate relevant fields, construct an immutable command or DTO, call the service boundary, and update the page from the accepted result. Clear draft state only after persistence succeeds.
```python
from dataclasses import field
from nicegui import binding, ui
async def submit() -> None:
if not all(field.validate() for field in (name, email)):
return
@binding.bindable_dataclass
class PageState:
selected_id: int | None = None
items: list = field(default_factory=list)
state = PageState()
ui.label().bind_text_from(state, "selected_id")
```
## File Upload Pattern
- Validate extension and size before storing.
- Delegate storage to a service method.
- Notify success and failure explicitly.
```python
async def handle_upload(e: ui.events.UploadEventArguments):
submit_button.disable()
try:
if e.size > 10 * 1024 * 1024:
raise ValueError("File too large")
if not e.name.endswith(".pdf"):
raise ValueError("Only PDF allowed")
await file_service.store(e.content.read(), e.name)
ui.notify(f"Uploaded: {e.name}", type="positive")
except ValueError as err:
ui.notify(str(err), type="negative")
ui.upload(on_upload=handle_upload, auto_upload=True)
user = await user_service.create(name=name.value, email=email.value)
ui.notify(f"Created {user.display_name}", type="positive")
name.set_value("")
email.set_value("")
except DuplicateEmailError:
email.error = "This email is already registered"
finally:
submit_button.enable()
```
## Form Submission Pattern
For asynchronous field validators, await the validator at the service boundary or maintain an explicit validation state; do not use the synchronous return value of `validate()` as proof that asynchronous validation completed.
- Bind UI inputs to dataclass fields.
- Perform validation in the service layer.
- Clear form state on success.
## Upload Mechanics
[`ui.upload`](https://nicegui.io/documentation/upload) wraps Quasar's `QUploader`. The tagged [`Upload` wrapper](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload.py) registers a POST route scoped to the current client and element. Its event order is:
1. `on_rejected` during browser-side file selection for Quasar restrictions.
2. `on_begin_upload` when the client starts a request.
3. `on_upload` once for each server-received file.
4. `on_multi_upload` after all files in that request have been converted.
`max_file_size`, `max_total_size`, `max_files`, and an `accept` prop improve client feedback, but NiceGUI's [security guidance](https://nicegui.io/documentation/section_security#examples_are_starting_points) identifies those restrictions as browser-side checks. Revalidate size, media type, content signature, filename policy, authorization, and storage quota on the server before persisting or parsing data.
In NiceGUI 3.16, `event.file` is a [`FileUpload`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload_files.py):
| Surface | Behavior |
| --- | --- |
| `name` | basename sanitized by NiceGUI; still untrusted display metadata |
| `content_type` | request-provided media type; not content verification |
| `size()` | synchronous byte count |
| `read()`, `text()`, `json()` | asynchronous full-content reads |
| `iterate(chunk_size=...)` | asynchronous chunks for bounded-memory processing |
| `save(path)` | asynchronous save to an application-selected path |
NiceGUI reads the incoming Starlette upload and keeps it in memory up to `MultiPartParser.spool_max_size`; larger files spill to a temporary file. This spool threshold controls memory versus disk, not the allowed upload size. Raising it increases per-upload memory pressure and should not be used as a validation mechanism.
```python
@binding.bindable_dataclass
class FormData:
name: str = ""
email: str = ""
from nicegui import events, ui
data = FormData()
ui.input("Name").bind_value(data, "name")
ui.input("Email").bind_value(data, "email")
async def on_submit():
async def handle_upload(event: events.UploadEventArguments) -> None:
file = event.file
if file.size() > 10 * 1024 * 1024:
ui.notify("File exceeds 10 MB", type="negative")
return
if file.content_type != "application/pdf":
ui.notify("Only PDF files are accepted", type="negative")
return
try:
await user_service.create_user(name=data.name, email=data.email)
ui.notify("User created", type="positive")
data.name = data.email = ""
except ValueError as err:
ui.notify(str(err), type="negative")
await file_service.store(chunks=file.iterate(), original_name=file.name)
except StorageQuotaError:
ui.notify("Storage quota exceeded", type="negative")
else:
ui.notify(f"Uploaded {file.name}", type="positive")
ui.button("Submit").on_click(on_submit)
uploader = ui.upload(
on_upload=handle_upload,
on_rejected=lambda: ui.notify("File rejected", type="negative"),
max_file_size=10 * 1024 * 1024,
auto_upload=True,
).props("accept=application/pdf")
```
## Real-Time Updates Decision
Generate the durable storage name independently from `file.name`, keep user-uploaded active content off the application origin, and apply content-specific scanning before downstream parsers consume the file. Call `uploader.reset()` when the product should clear QUploader's client-side queue after a completed or abandoned operation.
Use SSE for one-way status streaming.
Use WebSocket for bidirectional messaging.
## Element Updates And Refreshable Regions
SSE endpoint example:
Use the narrowest update mechanism that represents the change:
```python
@app.get("/events/status")
async def status_stream():
async def gen():
while True:
yield f"data: {await get_status()}\\n\\n"
await asyncio.sleep(1)
return StreamingResponse(gen(), media_type="text/event-stream")
```
| Change | Appropriate surface |
| --- | --- |
| one wrapper property | setter, binding, or property assignment supported by that wrapper |
| mutated option or row collection | wrapper helper or explicit `element.update()` |
| a bounded subtree whose structure changed | `@ui.refreshable` or `@ui.refreshable_method` |
| navigation to a different page | `ui.navigate` or `ui.sub_pages` |
## Background Work Pattern
The tagged [`refreshable` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/functions/refreshable.py) records every invocation as a target container. `refresh()` clears and recreates each matching target; it does not diff children. Arguments passed to `refresh()` replace prior positional arguments when non-empty and update prior keyword arguments.
- Start long jobs in FastAPI background tasks.
- Expose status via endpoint or streaming channel.
- Guard buttons against duplicate submissions during in-flight tasks.
A module-level refreshable called by multiple clients has multiple targets, so refreshing it can update all surviving targets. Define the decorated function inside the page, create a page-local decorated wrapper, or use a per-page object with `@ui.refreshable_method` when clients need independent refresh behavior.
## Explicit Refresh Pattern
For asynchronous refreshable functions:
Use @ui.refreshable and call refresh intentionally instead of polling unrelated state.
- `await region.refresh()` waits for all matching async refreshes to finish
- calling `region.refresh()` without awaiting schedules the async work in the background
- awaiting is appropriate when a button must remain disabled until rendering completes
- each refresh clears the old target before the new async render finishes, so provide a stable outer loading surface when an empty interval would be disruptive
```python
@ui.refreshable
async def item_list():
items = await service.list()
for item in items:
ui.label(item.name)
`ui.state()` is local storage indexed by call order inside one refreshable target. It can only be called inside a refreshable function, and conditional changes to state-call order can associate values with the wrong logical state. Use typed page state or bindable dataclasses when state identity must remain explicit.
ui.button("Refresh").on_click(lambda: item_list.refresh())
```
## Timers And Application Events
## Links
[`ui.timer`](https://nicegui.io/documentation/timer) is client-scoped. Its tagged [element implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/timer.py) waits for the client connection and cancels the current invocation when the element is deleted. `app.timer` is application-scoped and has no UI context of its own.
!!! info "Primary sources"
- [NiceGUI action events](https://nicegui.io/documentation/section_action_events)
- [FastAPI server-sent events](https://fastapi.tiangolo.com/advanced/server-sent-events/)
- [FastAPI WebSockets](https://fastapi.tiangolo.com/advanced/websockets/)
The tagged base [`Timer`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/timer.py) awaits each callback before scheduling the remainder of the interval, so one timer does not overlap its own invocations. A callback that takes longer than the interval causes the next iteration to begin without an additional delay. `deactivate()` pauses future invocations, while `cancel(with_current_invocation=True)` also cancels the current callback task and cannot be reversed.
Use timers for truly periodic observation, not to compensate for a missing event or explicit refresh. Polling intervals must account for query cost, number of connected clients, and process-local duplication under multiple workers.
[`Event`](https://nicegui.io/documentation/event) decouples long-lived Python producers from UI subscribers:
- `emit()` invokes subscribers without waiting for async callbacks to complete
- `call()` awaits all subscribers and propagates their failures to the caller
- `emitted(timeout=...)` waits for the next emission
- subscriptions created in a UI context are automatically removed when that client is deleted unless configured otherwise
The automatic unsubscribe behavior in the tagged [`Event` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/event.py) makes an application event suitable for connecting longer-lived models to page-local UI without retaining deleted clients. It remains process-local; use a broker or shared service for cross-worker fan-out.
## Execution Contexts
Choose an execution surface by workload and lifetime:
| Surface | Execution | Suitable for | Important constraint |
| --- | --- | --- | --- |
| async UI handler | event loop | non-blocking clients and short orchestration | blocking calls freeze all clients on that loop |
| `run.io_bound()` | shared thread pool | blocking file, HTTP, or SDK calls | cancellation does not necessarily stop the underlying thread operation |
| `run.cpu_bound()` | process pool | CPU-heavy pure computation | callable, arguments, result, and failures cross a pickle boundary |
| `background_tasks.create()` | event-loop task | detached async work owned by this process | canceled during shutdown unless tagged with `await_on_shutdown` |
| FastAPI `BackgroundTasks` | after an HTTP response | small route-triggered work | still belongs to the web process; not a durable queue |
| external worker or job queue | separate process or service | durable, retryable, resource-heavy jobs | requires explicit status, cancellation, and result contracts |
The tagged [`run` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/run.py) uses a thread pool for `io_bound` and a process pool for `cpu_bound`. For CPU work, prefer a module-level function with simple serializable arguments and return data rather than UI objects or closures. NiceGUI 3.16 inherits the platform multiprocessing start method unless `run.process_pool_start_method` is set before startup; `spawn` avoids unsafe fork behavior in a threaded process but does not inherit module state.
The tagged [`background_tasks` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/background_tasks.py) keeps strong references to running tasks, forwards unhandled exceptions to global exception handlers, and cancels ordinary tasks during shutdown. `create_lazy()` coalesces repeated work by name into the current run plus only the latest waiting coroutine; it is useful for refresh-style invalidation, not for work where every event must be processed.
## Live Update Transports
| Requirement | Default surface |
| --- | --- |
| update the initiating NiceGUI page | mutate elements or bound page state in its client context |
| notify all local clients of a page | iterate `app.clients(path)` and enter each `with client:` context |
| connect a long-lived Python producer to page subscribers | NiceGUI `Event` with page-local subscriptions |
| one-way HTTP event stream for an external/browser consumer | FastAPI SSE endpoint |
| custom bidirectional protocol independent of NiceGUI elements | FastAPI WebSocket endpoint |
| cross-worker or cross-instance broadcast | external broker plus a subscriber in each process |
FastAPI's [SSE support](https://fastapi.tiangolo.com/tutorial/server-sent-events/) uses a yielding route with `response_class=EventSourceResponse`. `ServerSentEvent` adds `event`, `id`, `retry`, and comment fields; event IDs support application-defined resume behavior through `Last-Event-ID`. FastAPI supplies keep-alive comments and headers that discourage proxy buffering and caching. The stream producer still owns authorization, disconnect-aware resource cleanup, replay semantics, and bounded buffering.
FastAPI [WebSockets](https://fastapi.tiangolo.com/advanced/websockets/) support text, bytes, and JSON in both directions. Catch `WebSocketDisconnect`, remove the connection from any local registry, and remember that an in-memory connection manager reaches only clients attached to the same process.
Do not use SSE or a custom WebSocket merely to update NiceGUI elements. Those transports do not automatically establish the target NiceGUI client context or synchronize its element tree.
## Concurrency And Feedback State
Disabling the initiating control communicates that work is active, but it is not a server-side concurrency guarantee. Also guard the handler or service with one of these policies:
- reject a second request while the operation is in flight
- coalesce duplicate refresh requests and keep only the latest invalidation
- serialize operations with a lock scoped to the affected entity or user
- make the service operation idempotent and return the existing result
For search, filtering, and other replaceable reads, an older request can complete after a newer request. Associate each request with a monotonically increasing generation or cancel the previous task, and only publish a result that still matches the current generation. Cancellation must still restore enabled/loading state in `finally`.
Every user-triggered asynchronous operation should expose a bounded state model such as `idle`, `running`, `succeeded`, `failed`, or `canceled`. Keep the error message near the action, preserve user input after expected failure, and do not convert unexpected programming errors into a generic success-like state.
## Source Index
!!! info "NiceGUI public documentation"
- [Pages and client connection](https://nicegui.io/documentation/page)
- [Action, events, execution, and error handling](https://nicegui.io/documentation/section_action_events)
- [Input and validation](https://nicegui.io/documentation/input)
- [Upload](https://nicegui.io/documentation/upload)
- [Refreshable UI](https://nicegui.io/documentation/refreshable)
- [Timer](https://nicegui.io/documentation/timer)
- [Application events](https://nicegui.io/documentation/event)
- [Storage scopes](https://nicegui.io/documentation/storage)
!!! info "NiceGUI `3.16.0` implementation"
- [Page builder and response phases](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/page.py)
- [Client lifecycle](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/client.py)
- [Outbox and reconnect replay](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/outbox.py)
- [Validation elements](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/mixins/validation_element.py)
- [Upload wrapper](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload.py)
- [Uploaded-file storage and access](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload_files.py)
- [Refreshable targets and local state](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/functions/refreshable.py)
- [Timer scheduling](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/timer.py)
- [Application event dispatch](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/event.py)
- [Thread and process execution](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/run.py)
- [Background-task lifecycle](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/background_tasks.py)
!!! info "FastAPI transports and tasks"
- [Server-sent events](https://fastapi.tiangolo.com/tutorial/server-sent-events/)
- [WebSockets](https://fastapi.tiangolo.com/advanced/websockets/)
- [Response background tasks](https://fastapi.tiangolo.com/tutorial/background-tasks/)
@@ -1,172 +1,71 @@
# NiceGUI Visual Styling And CSS
# NiceGUI Page Structure, Typography, And Scaling
Use this reference for cosmetic and presentational work: themes, color roles, utility classes, CSS properties, responsive layout, and static assets. For the mechanics of how a NiceGUI Python element maps to a Quasar Vue component, including props, events, slots, methods, teleported content, and wrapper-owned state, load [component mechanics](./component-mechanics.md).
Use this reference for the physical structure of a NiceGUI page: container geometry, Tailwind layout classes, spacing, overflow, responsive reflow, font loading, typography, and scale. Prefer NiceGUI's Python mechanics or Tailwind classes wherever they can express the requirement; custom CSS is the fallback, not a parallel styling path. For the mechanics of how a NiceGUI Python element maps to a Quasar Vue component, including props, events, slots, methods, teleported content, and wrapper-owned state, load [component mechanics](./component-mechanics.md).
For package boundaries, dependency direction, and page or component ownership, load [application architecture](./architecture.md).
## Visual Styling Boundary
## Page Structure Boundary
This page owns how an element looks and fits into a page after the correct component and behavior have been chosen. Typical concerns include:
This page owns how elements occupy and share space after the correct components and behavior have been chosen. Typical concerns include:
- application color roles and light or dark presentation
- width, height, spacing, alignment, wrapping, and overflow
- typography, borders, shadows, focus treatments, and state colors
- page shells, content-width constraints, columns, rows, and grid tracks
- width, height, spacing, alignment, wrapping, overflow, and scroll ownership
- font resources, font families, type sizes, weights, line height, and line length
- responsive page composition and stable control dimensions
- reusable application classes, CSS custom properties, and static assets
- rem-based sizing, browser text enlargement, and explicit element scaling
- exceptional CSS that cannot be expressed through Python mechanics or Tailwind classes
The companion [component mechanics](./component-mechanics.md) reference owns how behavior crosses framework boundaries. Use it when the question is whether a value belongs in a constructor, Quasar prop, Vue event, slot, method, binding, or teleported popup.
## Visual Styling Workflow
## Precedence: Python, Then Tailwind, Then CSS
Escalate only as far as the visual requirement needs:
Apply this order to every structural requirement:
1. Use a NiceGUI constructor argument when it directly expresses appearance, such as an icon, color, or size.
2. Use documented Quasar appearance props through `.props(...)` for component variants such as `outlined`, `rounded`, or `dense`.
3. Use Tailwind classes for page structure and common visual utilities.
4. Use Quasar utility classes for Quasar spacing, typography, semantic colors, visibility, and positioning.
5. Use `.style(...)` for a calculated runtime value or a short-lived visual probe.
6. Move stable or repeated declarations into a scoped static stylesheet under an application-owned class.
1. Use NiceGUI's Python composition and component APIs: containers such as `ui.row`, `ui.column`, and `ui.grid`, constructor arguments, documented properties, slots, and wrapper methods.
2. Add Tailwind classes through `.classes(...)` for width, tracks, spacing, alignment, wrapping, overflow, responsive changes, typography, and other physical presentation.
3. Use Quasar props or helper classes when the requirement belongs specifically to a Quasar component and NiceGUI exposes that boundary.
4. Use `.style(...)` only for a calculated runtime value that cannot be represented by the available APIs or utility classes.
5. Add scoped static CSS only when all preceding layers cannot express the requirement without relying on unsupported component internals.
Stop when the required presentation is achieved. If a proposed rule needs selectors such as `.q-field__control`, changes a popup's mounting or positioning behavior, or depends on generated Vue markup, resolve the component mechanics first instead of compensating with CSS.
Do not create a stylesheet merely to rename or group utilities that fit cleanly in `.classes(...)`. Reuse a Python component or helper when a class sequence repeats. Before adding CSS, identify the unsupported requirement it solves; if the rule needs selectors such as `.q-field__control`, changes popup positioning, or depends on generated Vue markup, resolve the component mechanics first instead of compensating with CSS.
```python
ui.select(
options=items,
label="Item",
).props(
"outlined popup-content-class=app-item-menu"
).classes(
"app-item-select w-full md:max-w-md"
(
ui.select(options=items, label="Item")
.props("outlined")
.classes("w-full md:max-w-md rounded")
)
```
```css
.app-item-select {
border-radius: 0.25rem;
}
## Physical Layout Model
.app-item-menu {
max-height: min(24rem, 60dvh);
}
```
Four layout decisions control most NiceGUI page structure:
## Application Themes With NiceGUI And Quasar
| Decision | Typical declarations | Failure when omitted |
| --- | --- | --- |
| outer constraint | `w-full`, `max-w-*`, `mx-auto`, `px-*` | content touches viewport edges or becomes unreadably wide |
| track sizing | `flex-1`, `shrink-0`, `grid-cols-*`, `minmax(0, 1fr)` | sidebars collapse or content forces tracks wider than the viewport |
| intrinsic minimums | `min-w-0`, `min-h-0` | flexible children refuse to shrink and create page-level overflow |
| overflow owner | `overflow-auto`, `overflow-x-auto`, `overflow-hidden` | multiple nested scrollers or clipped interactive content |
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()`](https://nicegui.io/documentation/colors#app-wide-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.
```python
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()`](https://nicegui.io/documentation/colors) only when one page intentionally overrides the app palette. It is page-scoped and takes precedence over `app.colors()`:
```python
@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()`](https://nicegui.io/documentation/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.
```python
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:
```css
: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.
NiceGUI rows and columns provide component structure, while their `.classes(...)` values define the physical constraints. Prefer explicit Tailwind `p-*` and `gap-*` classes for local container spacing. NiceGUI's `--nicegui-default-padding` and `--nicegui-default-gap` variables, both `1rem` by default, are CSS-level exceptions for changing the framework-wide baseline rather than one container.
## Structural Styling With Tailwind
Use standard [Tailwind utility classes](https://tailwindcss.com/docs/utility-first) for page and component structure:
NiceGUI's `.classes()` method attaches Tailwind-compatible classes directly to the rendered element. The structural categories used most often are:
- 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
| Concern | Representative classes |
| --- | --- |
| display and tracks | `flex`, `grid`, `grid-cols-1`, `md:grid-cols-2` |
| growth and shrinkage | `flex-1`, `grow`, `shrink-0`, `basis-*` |
| dimensions | `w-full`, `h-full`, `min-w-0`, `max-w-6xl`, `size-10` |
| spacing | `gap-4`, `px-4`, `py-6`, `mx-auto`, `space-y-3` |
| alignment | `items-start`, `items-center`, `justify-between`, `self-stretch` |
| wrapping and overflow | `flex-wrap`, `whitespace-nowrap`, `overflow-auto`, `truncate` |
| positioning | `relative`, `absolute`, `sticky`, `inset-*`, `z-*` |
| responsive changes | `md:flex-row`, `lg:grid-cols-3`, `xl:max-w-7xl` |
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.
The [Tailwind width](https://tailwindcss.com/docs/width) and [maximum-width](https://tailwindcss.com/docs/max-width) references distinguish fixed spacing-scale widths, fractions, viewport units, and container-scale constraints. A centered shell normally combines its responsibilities explicitly:
```python
with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4"):
@@ -177,60 +76,113 @@ with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4"):
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.
`w-full` fills available inline space, `max-w-6xl` caps line and panel length, `mx-auto` centers the shell, and `px-4` retains edge space below the cap. Inside the row, `shrink-0` protects the sidebar and `min-w-0` allows the flexible content track to become narrower than its intrinsic content.
Tailwind's [responsive variants](https://tailwindcss.com/docs/responsive-design) are mobile-first. Unprefixed classes apply at every size; `md:*` and larger prefixes apply from their minimum width upward. In NiceGUI's default Tailwind setup, verify available classes against the framework version bundled by the installed NiceGUI release. Optional [UnoCSS presets](https://nicegui.io/documentation/section_styling_appearance#unocss_engine) are intentionally not fully compatible with Tailwind, and Tailwind CSS layers are one documented difference.
### Combine Tailwind With Quasar Utilities Deliberately
NiceGUI's `.classes()` accepts both Tailwind utilities and the CSS helpers bundled with Quasar. Keep Tailwind as the default for application layout and responsive structure, but use Quasar utilities when they express a Quasar-owned or framework-semantic concern more directly:
NiceGUI's `.classes()` accepts both Tailwind utilities and CSS helpers bundled with Quasar. Tailwind remains the default for application layout and responsive structure; Quasar helpers are useful when dimensions should follow Quasar's component conventions:
- [`q-m*` and `q-p*` spacing classes](https://quasar.dev/style/spacing) when spacing should follow Quasar's component scale
- [typography helpers](https://quasar.dev/style/typography), such as `text-h6`, `text-subtitle2`, and `text-weight-medium`, for text that should follow Quasar's type system
- [color palette classes](https://quasar.dev/style/color-palette), such as `text-primary`, `bg-positive`, and `text-negative`, so semantic colors track the palette configured by `app.colors()` or `ui.colors()`
- [visibility helpers](https://quasar.dev/style/visibility), such as `gt-sm` and `lt-md`, when visibility should use Quasar's configured breakpoints
- [positioning helpers](https://quasar.dev/style/positioning), such as `absolute-top-right`, when positioning content relative to a Quasar component
- [size and overflow helpers](https://quasar.dev/style/other-helper-classes), such as `fit`, `full-width`, and `overflow-auto`, when matching Quasar layout behavior
Mix the two systems by concern, not by writing competing declarations for the same CSS property. For example, `w-full q-pa-md text-primary` uses Tailwind for width and Quasar for component-scale padding and semantic color. Do not combine `p-4` with `q-pa-md`, or Tailwind and Quasar visibility helpers, on the same element; their cascade order can make the result version-dependent and difficult to review.
Do not assign the same property through both systems on one element. For example, `w-full q-pa-md` uses Tailwind for width and Quasar for component-scale padding; adding `p-4` would create competing padding declarations. The same rule applies to Tailwind and Quasar visibility helpers or to Tailwind font sizes and Quasar heading classes.
```python
with ui.card().classes("w-full max-w-2xl q-pa-md"):
ui.label("Inventory summary").classes("text-h6 text-primary")
ui.label("Review required").classes("text-negative text-weight-medium")
ui.label("Inventory summary").classes("text-h6")
ui.label("12 locations").classes("text-subtitle2 text-weight-medium")
```
Quasar utilities are global classes, so they need no Vue-specific translation before being passed to `.classes()`. Confirm the available helpers and breakpoints against the Quasar version bundled by the installed NiceGUI release.
Tailwind and Quasar do not share breakpoint thresholds. Tailwind's defaults begin `sm` at `40rem` and `md` at `48rem`; Quasar defines `sm` from `600px` and `md` from `1024px`. Keep one breakpoint system responsible for a given layout transition, and confirm the bundled framework versions before relying on exact thresholds.
## Fine Tuning With Static Stylesheets
## CSS As A Last Resort
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:
Do not move stable geometry into a stylesheet simply because a Tailwind class string is long. Tailwind arbitrary values can express constraints such as `minmax(...)`, `min(...)`, aspect ratios, and dynamic viewport units while keeping the rule visible beside the Python structure that owns it.
```python
ui.select(...).props("popup-content-class=app-item-menu").classes(
"app-item-select w-full md:max-w-md"
)
with ui.element("main").classes(
"grid min-h-0 "
"grid-cols-[minmax(14rem,20rem)_minmax(0,1fr)]"
):
sidebar()
workspace().classes("min-w-0")
ui.select(...).props(
'outlined popup-content-class="max-h-[min(24rem,60dvh)] overflow-y-auto"'
).classes("w-full md:max-w-md")
```
```css
.app-item-select {
--app-field-accent: #176b5b;
}
Use `.style()` only when a value is calculated at runtime and no class or component property can represent it. Keep the override on the narrowest element and do not promote it to a shared stylesheet unless it becomes a genuine cross-component rule.
.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.
Static CSS remains appropriate for browser-level facilities such as `@font-face`, selectors or pseudo-elements with no available utility, and integration with markup that cannot receive classes. Attach an application-owned class through `.classes()` or a documented Quasar prop, then scope the exceptional rule beneath that class.
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.
## Fonts And Typography
Typography affects physical layout because font metrics determine line breaks, control height, baseline alignment, and the intrinsic width of labels. Treat font loading and the type scale as structural dependencies rather than late decoration.
### Font Families And Loading
Tailwind provides `font-sans`, `font-serif`, and `font-mono`, and supports custom family utilities as documented by [Tailwind font family](https://tailwindcss.com/docs/font-family). Quasar's [typography reference](https://quasar.dev/style/typography) documents its embedded Roboto default and its heading, weight, alignment, wrapping, and case helpers.
For an application-owned typeface, `@font-face` is one of the browser-level cases that warrants CSS. Mount the font with other static assets and declare it once in the shared stylesheet. [MDN `@font-face`](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face) recommends WOFF2 for modern web delivery; `font-display: swap` keeps text available while the resource loads.
```css
@font-face {
font-family: "App Sans";
src: url("/static/fonts/app-sans.woff2") format("woff2");
font-display: swap;
font-style: normal;
font-weight: 400 700;
}
.app-shell {
font-family: "App Sans", sans-serif;
}
```
Include the real weight range supplied by the font file. Requesting an unavailable weight makes the browser synthesize it and can alter text width. Keep a fallback family so failed or delayed font requests do not leave text unavailable.
### Type Size And Line Height
Tailwind's [font-size utilities](https://tailwindcss.com/docs/font-size) pair named rem-based sizes such as `text-sm`, `text-base`, and `text-xl` with default line heights. Combined forms such as `text-sm/6` set size and line height together. Separate `leading-*`, `font-*`, and text-alignment utilities refine those dimensions.
```python
with ui.column().classes("w-full max-w-[65ch] gap-3"):
ui.label("Inventory summary").classes("text-2xl/8 font-semibold")
ui.label("Counts by location and storage area").classes("text-base/7")
```
Prefer a small named hierarchy over unrelated one-off sizes. Use `rem`-based utilities so browser font preferences and page zoom remain meaningful, and use a character-based maximum width such as `max-w-[65ch]` for long prose. Avoid viewport-width font sizing: text should reflow at narrow widths rather than shrink to preserve one line.
`em` dimensions inherit and can compound through nested elements; `rem` dimensions refer to the root element and avoid that compounding. The [MDN font-size reference](https://developer.mozilla.org/en-US/docs/Web/CSS/font-size) describes both behaviors and recommends relative sizing for accessibility.
## Scaling Boundaries
The word "scale" can refer to different browser mechanics. They are not interchangeable:
| Mechanism | Participates in layout | Appropriate use |
| --- | --- | --- |
| responsive classes and reflow | yes | normal page adaptation across available widths |
| relative font and spacing units | yes | coherent type and spacing changes that respect browser settings |
| browser zoom | yes, at the document level | user-controlled magnification that the page must tolerate |
| CSS `zoom` | yes | exceptional magnification of a bounded region |
| `transform: scale(...)` | no | transient visual emphasis or a deliberately overlaid preview |
Responsive reflow through Python composition and Tailwind classes is the default for page structure. A narrower page should stack tracks, wrap controls, and retain readable text rather than shrink the entire interface.
[CSS `zoom`](https://developer.mozilla.org/en-US/docs/Web/CSS/zoom) changes the size used by layout, so surrounding content is recalculated. [`transform: scale()`](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/scale) changes only painting; neighboring elements retain the unscaled geometry, and enlarged content can overlap or overflow its box. Treat both as exceptional effects after responsive widths, gaps, and breakpoints have been exhausted.
Stable fixed-format regions such as boards, diagrams, and previews need an explicit box before their contents scale. Combine `aspect-ratio`, a bounded inline size, and local overflow rules so transformed content cannot resize surrounding controls. Scaling animations should respect `prefers-reduced-motion`.
## Responsive Layout
Support these layouts only:
@@ -239,7 +191,7 @@ Support these layouts only:
- 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.
Build the mobile layout first, then add one desktop breakpoint when a row or grid needs more space. Unprefixed Tailwind classes define the mobile baseline; breakpoint-prefixed classes alter it at larger widths. Prefer flex wrapping and fluid grids before adding another breakpoint. Use Tailwind classes for page layout and Quasar props for component density and behavior.
```python
with ui.row().classes("w-full flex-wrap gap-4 lg:flex-nowrap items-start"):
@@ -249,12 +201,15 @@ with ui.row().classes("w-full flex-wrap gap-4 lg:flex-nowrap items-start"):
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
Height needs an explicit ownership chain. `h-full` only resolves when the containing block has a definite height; viewport-bound workspaces usually need a defined outer height and `min-h-0` on nested flex or grid tracks before an inner `overflow-auto` region can scroll. Prefer dynamic viewport units such as `dvh` for browser UI that changes the visible mobile viewport.
- 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.
## Loading Exceptional CSS And Static Assets
- Keep ordinary layout and typography in Python mechanics and Tailwind classes rather than creating a stylesheet.
- When exceptional CSS is required, mount and link it once from the composition layer rather than injecting it from individual pages.
- Keep custom dimensions and font families in named variables or application classes.
- Avoid broad rules against Quasar internals.
- Mount referenced assets in the composition layer.
- Mount referenced stylesheets, fonts, and other assets in the composition layer.
- Verify mount paths, reverse-proxy rewrites, and cache behavior.
```python
@@ -283,23 +238,29 @@ Check each completed page at these three viewports:
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.
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. Repeat the checks with browser zoom or text enlargement, a delayed font request, long labels, validation messages, and loaded content. Watch for unexpected page-level horizontal scrolling, nested scroll regions, clipped focus outlines, and layout shifts when the webfont replaces its fallback.
## Sources
!!! info "Primary sources"
- [NiceGUI element styling and props](https://nicegui.io/documentation/element)
- [NiceGUI binding properties](https://nicegui.io/documentation/section_binding_properties)
- [NiceGUI color theming](https://nicegui.io/documentation/colors)
- [NiceGUI dark mode](https://nicegui.io/documentation/dark_mode)
- [NiceGUI styling and appearance](https://nicegui.io/documentation/section_styling_appearance)
- [Quasar components](https://quasar.dev/vue-components)
- [Quasar spacing classes](https://quasar.dev/style/spacing)
- [Quasar typography helpers](https://quasar.dev/style/typography)
- [Quasar breakpoints](https://quasar.dev/style/breakpoints)
- [Quasar visibility helpers](https://quasar.dev/style/visibility)
- [Quasar positioning helpers](https://quasar.dev/style/positioning)
- [Quasar color palette and runtime brand variables](https://quasar.dev/style/color-palette)
- [Quasar dark mode](https://quasar.dev/style/dark-mode)
- [Quasar size and overflow helpers](https://quasar.dev/style/other-helper-classes)
- [Quasar field](https://quasar.dev/vue-components/field/)
- [Quasar select](https://quasar.dev/vue-components/select/)
- [Tailwind width utilities](https://tailwindcss.com/docs/width)
- [Tailwind maximum-width utilities](https://tailwindcss.com/docs/max-width)
- [Tailwind font-family utilities](https://tailwindcss.com/docs/font-family)
- [Tailwind font-size utilities](https://tailwindcss.com/docs/font-size)
- [Tailwind responsive design](https://tailwindcss.com/docs/responsive-design)
- [MDN `zoom`](https://developer.mozilla.org/en-US/docs/Web/CSS/zoom)
- [MDN `@font-face`](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face)
- [MDN `font-size`](https://developer.mozilla.org/en-US/docs/Web/CSS/font-size)
- [MDN `zoom`](https://developer.mozilla.org/en-US/docs/Web/CSS/zoom)
- [MDN `scale()`](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/scale)
@@ -1,39 +1,276 @@
# Troubleshooting and Quality Gates
# NiceGUI Troubleshooting And Quality Evidence
## Troubleshooting
Use this reference to identify the layer that owns a NiceGUI failure and the evidence needed to distinguish similar symptoms. It covers behavior verified against NiceGUI `3.16.0`; browser, Quasar, Vue, FastAPI, Socket.IO, Uvicorn, and proxy behavior must also be checked against the versions deployed by the target application.
### Upload Errors
The companion [interaction mechanics](./interaction-patterns.md) page defines normal lifecycle, validation, upload, refresh, timer, task, and transport behavior. [Component mechanics](./component-mechanics.md) covers Quasar props, events, slots, wrapper models, and frontend payload mapping. [FastAPI and Uvicorn startup](./fastapi-uvicorn-startup.md) covers import identity, workers, reload, and deployment topology.
- Validate extension and size before storage.
- Catch expected exceptions and return negative notifications.
- Log unexpected exceptions with request context.
## Diagnostic Index
### UI Race Conditions
| Symptom | Likely owner | Discriminating evidence |
| --- | --- | --- |
| upload rejected before handler runs | Quasar `QUploader` or browser selection rules | `on_rejected` fires; no upload POST reaches the server |
| upload request returns `400` or `413` | proxy, ASGI multipart parsing, NiceGUI upload route, or application validation | HTTP status and response body from the upload request; proxy and server logs |
| page shows a response-timeout error | async page construction before client connection | warning naming `response_timeout`; page builder timing and `connected()` boundary |
| update appears only after reload | wrong client context, unobserved plain mutation, or missing explicit update | target `Client`, element deletion state, outbox traffic, and wrapper update call |
| update reaches one tab but not another | private page element tree or process-local fan-out | client IDs, page paths, worker identity, and `app.clients(path)` iteration |
| updates disappear after a brief network interruption | client deleted after reconnect timeout or outbox replay unavailable | disconnect/delete timestamps, reconnect timeout, next message ID, and reload log |
| old query result replaces a newer one | concurrent async completion race | request generation, start/end timestamps, query identity, and publish order |
| all clients pause during one action | blocking work on the event loop | event-loop lag and stack or profile showing synchronous I/O or CPU work |
| callback runs repeatedly after navigation | duplicate timer, event subscription, or lifecycle registration | registration count, client IDs, delete handlers, and task names |
| user state leaks across tabs or users | incorrect storage scope or module-level mutable state | storage scope, session ID, tab ID, process ID, and object identity |
| URL changes but content or state does not | History API used without a route/content transition | `pushState`/`replaceState` call versus `ui.navigate.to` or sub-page routing |
| changed CSS or image remains stale | static cache lifetime or proxy/browser cache | response URL, `Cache-Control`, cache source in developer tools, and content version |
| exception is logged but no page feedback appears | exception occurred outside an active UI slot or after the client was deleted | exception handler invoked, current client/slot, task owner, and element state |
- Disable triggering controls during async work.
- Remove duplicate timers and listeners targeting the same state.
- Ensure service call ordering is deterministic before render updates.
Start with the smallest boundary that can explain the symptom. Browser developer tools establish whether an event, upload, static request, or socket message crossed the network. Server logs establish whether the page, client, handler, task, or service received it. Durable data inspection establishes whether the accepted operation committed independently of the UI.
### Asset Caching
## Upload Failures
- Confirm static mount and proxy rewrite correctness.
- Add cache-busting query strings for changed assets.
- Avoid per-page CSS injection.
[`ui.upload`](https://nicegui.io/documentation/upload) is a Quasar uploader backed by an element-specific NiceGUI POST route. The tagged [`Upload` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload.py) resolves the `client_id` and element ID from the route before converting each Starlette upload to `event.file`.
### Navigation and State Drift
### Rejected Before Transfer
- Avoid global mutable UI state.
- Keep state request-scoped or service-managed.
- Rehydrate page data during route load.
`max_file_size`, `max_total_size`, `max_files`, and the Quasar `accept` prop operate in the browser. A rejection at this stage calls `on_rejected`; it does not prove that the server would reject an equivalent direct request. An unexpected rejection commonly comes from MIME patterns, file-count state retained in the uploader queue, or size units that differ from the intended policy.
## Production Readiness Gate
Useful evidence includes the selected file's browser-reported type and size, current queue contents, configured Quasar props, and whether `on_begin_upload` or a network request occurs. Reset the uploader queue only when clearing previous selections is the intended product behavior.
Pass all checks before shipping:
### Transfer Or Multipart Failure
- Structure: one-way dependencies between pages, components, and services.
- Responsiveness: UI validated at both small and large viewport widths.
- Accessibility: labels and actions are clear and readable.
- Reliability: validation and exception paths surface user feedback.
- Maintainability: repeated UI patterns are extracted; business logic remains in services.
If the POST begins but the upload handler does not run, inspect the HTTP status before changing page code:
If any check fails, return to the workflow step that owns that concern.
| Response | Common boundary |
| --- | --- |
| `404` | stale or deleted element/client route, incorrect proxy prefix, or navigation during transfer |
| `400` | malformed multipart body, missing `client_id`, missing element ID, or no matching uploader element |
| `413` | reverse-proxy or ASGI request-size limit |
| `422` | route or dependency validation outside the normal NiceGUI upload route |
| `5xx` | multipart conversion, temporary storage, application handler, or downstream service failure |
The tagged [`FileUpload` conversion](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload_files.py) keeps small uploads in memory and spills larger ones to a temporary file after Starlette's `MultiPartParser.spool_max_size`. This is a buffering threshold, not an acceptance limit. Concurrency multiplies memory and temporary-disk pressure, so record file size, concurrent upload count, process memory, temporary filesystem capacity, and proxy limits together.
### Accepted But Unsafe Or Corrupt
`file.name` is reduced to its basename by NiceGUI, and `file.content_type` comes from the request. Neither establishes safe content. Server-side acceptance should record the authoritative byte size and verify content signature, parser behavior, quota, authorization, and application-selected destination. Use an independently generated storage key and keep active user content off the main application origin.
When downstream parsing fails, distinguish transport completion from domain acceptance. A successful upload POST can still produce a rejected document. Preserve an operation ID or storage record so logs and user feedback identify the same attempt without logging file content or sensitive form fields.
## Initial Page Response Failures
The tagged [`page` wrapper](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/page.py) gives async page construction `response_timeout` seconds, three by default, to finish or signal that it is waiting for the client connection. If neither happens, NiceGUI cancels the page task, deletes that client, logs a warning, and serves a terminal `500` page through a fresh client.
Increasing `response_timeout` can be appropriate for bounded, unavoidable initial construction, but it does not make long I/O responsive. The relevant timing split is:
- code before `await ui.context.client.connected()` delays the initial HTTP response
- code after that await runs with a connected browser and can progressively update the page
- synchronous blocking work in either phase can still stall the event loop
Capture elapsed time around dependencies, database calls, remote clients, serialization, and component construction. A timeout with low service latency may indicate a page builder waiting on a condition that itself requires the browser connection.
Synchronous page-builder exceptions and async exceptions raised before the response is built can render an `app.on_page_exception` page. That handler is synchronous in NiceGUI 3.16. A returned FastAPI `Response` bypasses normal page rendering. Do not assume a global `app.on_exception` handler can reconstruct a failed initial element tree.
## Connection, Reconnect, And Deleted Clients
The tagged [`Client` lifecycle](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/client.py) separates a socket disconnect from deletion. On disconnect, NiceGUI invokes disconnect handlers and waits for `reconnect_timeout`; a successful handshake cancels deletion. If no connection returns, NiceGUI closes tab storage as needed, invokes delete handlers, removes elements and bindings, stops the outbox, and removes the client from `Client.instances`.
### Stale Client Writes
Holding an element, slot, timer, or client in a long-lived object can outlive the page that created it. Writes after deletion trigger NiceGUI's deleted-client warning and cannot produce a valid browser update. Before publishing detached work, retain the intended client deliberately and check `client.is_deleted` or membership in the current client set. A durable job result should be written to durable state even when its original page no longer exists; a later page load can rehydrate it.
Do not treat `on_disconnect` as final resource disposal. It also runs for reconnectable interruptions. Page-owned cleanup belongs in `on_delete`; transport telemetry and reversible status belong in `on_disconnect` and `on_connect`.
### Reconnect Replay And Reload
The tagged [`Outbox`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/outbox.py) retains recent element updates and messages. During handshake, the browser provides the next message ID it expects. NiceGUI rewinds retained history and replays from that ID. If the ID is no longer available because of age or `message_history_length`, NiceGUI reloads the page.
This mechanism explains several superficially similar outcomes:
| Outcome | Interpretation |
| --- | --- |
| short interruption, state continues | client survived and required messages remained in history |
| interruption followed by reload | rewind target was unavailable or browser initiated a reload |
| interruption followed by fresh page state | original client was deleted and route rebuilt a new element tree |
| durable operation duplicated after reconnect | application command lacked idempotency; outbox replay is not a transaction protocol |
Correlate client ID, document or tab identity, message IDs, disconnect duration, reconnect timeout, and process ID. A load-balanced multi-worker deployment also needs compatible session affinity and shared application state; an in-memory client exists only in the worker that created it.
## Missing Or Misrouted Updates
Each page client owns a private element tree. Mutating an element affects that element's client; mutating a plain list or model that has no active binding does not enqueue a browser update by itself. Check these in the owning layer:
- the element has not been deleted or replaced by a refresh
- the handler runs in the intended client's slot context
- the wrapper property is bindable or followed by its documented helper or `update()`
- the refreshable target belongs to the intended client
- application-wide producers iterate the intended `app.clients(path)` and enter each client context
- process-local events are not assumed to reach clients connected to another worker
A module-level `@ui.refreshable` can accumulate targets from several clients. Its tagged [`refresh()` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/functions/refreshable.py) clears and rebuilds every matching surviving target. Unexpected cross-client refresh therefore indicates target scope, not shared DOM. Unexpectedly missing refresh often indicates that the target container was deleted or the code refreshed a different decorated instance.
## Async Races And Duplicate Actions
NiceGUI event handlers that return awaitables are scheduled as background tasks. Disabling a button reduces normal repeated clicks but does not serialize direct requests, reconnect replays, keyboard submission, another control, or another client.
### Completion-Order Races
For replaceable reads such as search, an older request can finish after a newer request and overwrite its result. Record a generation or request key when work starts and compare it immediately before publishing. Cancellation can reduce wasted work but is not sufficient when the underlying thread, remote service, or database operation cannot be canceled.
For writes, define the service-level policy explicitly: lock, optimistic entity version, idempotency key, conflict response, or accepted duplicate semantics. UI busy state is feedback, not concurrency control.
### Refresh Races
Each refreshable invocation owns a target container. Refresh clears that target before recreating children; concurrent refreshes can therefore interleave service reads and rendering. Await a refresh when the triggering action depends on completion, serialize refreshes for one target, or use a latest-generation policy for replaceable data. Keep long-lived loading and error indicators outside the cleared target if they must remain stable.
### Observable Evidence
For an asynchronous interaction, logs are most useful when they contain operation ID, client ID, user or tenant identifier where safe, entity ID, request generation, start and finish time, outcome, and exception type. Avoid recording secrets, raw uploaded content, session cookies, or full form payloads.
## Blocking Work And Event-Loop Lag
An `async def` callback does not make synchronous work non-blocking. CPU-heavy loops, synchronous HTTP clients, filesystem calls, image or document parsers, and blocking database drivers executed on the event loop delay socket heartbeats, all clients' event handlers, timers, page responses, and outbox delivery.
Use the execution boundary defined in [interaction mechanics](./interaction-patterns.md#execution-contexts): non-blocking async APIs in the event loop, `run.io_bound()` for blocking I/O, `run.cpu_bound()` for serializable CPU work, or an external worker for durable jobs. A thread keeps the loop responsive but does not remove memory, timeout, thread-safety, or cancellation constraints. A process pool adds serialization and process-start constraints.
Evidence for event-loop blocking includes simultaneous latency across unrelated clients, delayed timers or Socket.IO heartbeats, event-loop lag metrics, and a stack or profile inside synchronous work. A single slow awaited network request that yields control does not by itself block other clients.
## Timer, Listener, And Task Duplication
Repeated callbacks usually originate at registration, not dispatch. Common ownership mistakes include:
- creating `ui.timer` repeatedly during a refresh while retaining the old timer outside the cleared container
- registering an application timer or lifecycle handler during a per-client page build
- subscribing a long-lived `Event` outside a UI context without later unsubscribing
- starting a new consumer task on every reconnect instead of once at application startup
- reloading a development process while an external scheduler still targets both old and new instances
In NiceGUI 3.16, a page-scoped `ui.timer` waits for its client connection and is canceled when its element is deleted. An `app.timer` is process-scoped. An `Event` subscription made inside a UI context is automatically removed on client deletion by default; one made outside UI context has no automatic client owner. Application lifecycle handlers and external broker consumers need an application-level owner and shutdown path.
Record timer or task name, registration site, process ID, client ID when applicable, activation state, and cancellation reason. Count registrations directly rather than inferring duplication from repeated business effects, which could also come from retries or multiple workers.
## Storage And Navigation Drift
State drift often comes from assigning data to a scope with the wrong lifetime:
| Unexpected behavior | Scope to inspect |
| --- | --- |
| state disappears on reload or route navigation | `app.storage.client` or page-local Python object |
| state unexpectedly follows another tab | `app.storage.user`, `browser`, or module-global state |
| state is missing immediately after page construction | `app.storage.tab` accessed before `client.connected()` |
| state differs between workers | local file storage, in-memory tab state, or module-global state |
| browser storage mutation raises or is ignored | `app.storage.browser` changed after response construction |
The tagged [`storage` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/storage.py) persists `user` and `general` scopes locally by default or in Redis when configured. Tab storage is in-memory unless Redis is configured. The signed browser cookie identifies a user storage record; storage scope is not authorization, and persisted identifiers must still be checked against the authenticated principal and tenant.
[`ui.navigate.to`](https://nicegui.io/documentation/navigate) opens a route, client element anchor, or external URL. With `ui.sub_pages`, a relative same-app route can be handled within the current client. `ui.navigate.history.push()` and `.replace()` only change browser history state and the visible URL; they do not invoke a page builder or rehydrate content. A URL/content mismatch after `pushState` is therefore expected unless application code also owns the content transition.
A full navigation or reload creates a new page client, so page-local objects and client storage are not durable navigation state. Encode shareable state in route or query parameters, place tab- or user-lifetime state in the matching storage scope, and reload authoritative data from services rather than retaining element instances globally.
## Static Assets, Media, And Cache Boundaries
The tagged [`Client.build_response()`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/client.py) marks NiceGUI page and Markdown responses `Cache-Control: no-store`. A proxy that caches page HTML against that header can serve stale client IDs, initial state, or user-specific content and is misconfigured.
Static files intentionally use a different policy. In NiceGUI 3.16:
- `app.add_static_files()` and `app.add_static_file()` default to `Cache-Control: public, max-age=3600`
- `max_cache_age=0` requests immediate revalidation behavior but does not create a private authorization boundary
- media routes support byte-range streaming and should be used for seekable audio or video
- static and media directory helpers explicitly expose their contents without per-file application authorization
- `single_use=True` removes a route after the first handled request in one process; it is not a secure, distributed, or retry-safe download grant
The implementation is defined by [`app.add_static_*` and `app.add_media_*`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/app/app.py) and [`CacheControlledStaticFiles`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/staticfiles.py).
For stale assets, inspect the actual response in browser developer tools: final URL after proxy rewriting, status, `Cache-Control`, `ETag` or modification metadata, service-worker involvement, and whether the response came from memory, disk, intermediary, or origin. Prefer content-versioned URLs for immutable assets. Query-string cache busting works only when every cache key includes the query and the origin serves the updated bytes.
Security-sensitive files belong behind an authenticated FastAPI route or object-store authorization mechanism with private cache policy. A hard-to-guess static URL is not access control, and public cache headers can retain content beyond logout or permission changes.
## Exception Surfaces
NiceGUI exceptions have different user-feedback capabilities according to where they occur:
| Failure surface | Handler path | UI context available |
| --- | --- | --- |
| page builder before response | `app.on_page_exception`, FastAPI handlers, then global exception handlers | fresh error-page client for synchronous page handler |
| UI event or awaited callback in an element slot | client in-page exception handlers plus global handlers | originating slot while client remains alive |
| timer or NiceGUI background task | global handler; in-page handler only when task retained an active slot context | depends on captured context and client lifetime |
| `Event.emit()` subscriber | exception forwarded to global handling | subscriber's captured slot when available |
| `Event.call()` subscriber | exception propagates to caller | caller decides feedback and transaction behavior |
| FastAPI route outside NiceGUI page UI | FastAPI exception handling | no implicit NiceGUI element context |
The tagged [`app.handle_exception()`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/app/app.py) first invokes a client's in-page exception handling when a client and slot are active, then invokes global exception handlers. Unexpected exceptions should retain a traceback and correlation ID in server logs. User feedback should be specific for expected domain failures and generic for unexpected failures, without exposing internals.
An exception notification is not recovery by itself. Restore busy state in `finally`, preserve user input after a recoverable failure, reconcile uncertain write outcomes from the authoritative store, and stop publishing to deleted clients.
## Security-Sensitive Boundaries
The following client-visible mechanisms improve usability but do not enforce policy:
- disabled or hidden controls
- Quasar input rules and upload restrictions
- route names, element IDs, client IDs, or unguessable-looking static paths
- values retained in page, tab, browser, or user storage
- custom JavaScript validation or transformed event payloads
Authorization, tenant boundaries, accepted fields, type and range checks, optimistic concurrency, upload inspection, and durable write constraints belong on the server. For every mutation, identify the authenticated principal independently of browser-submitted ownership fields.
Do not interpolate untrusted values into raw `ui.html`, Vue templates, JavaScript, or style content. Use wrapper text/value APIs and structured serialization. Review proxy trust, forwarded-prefix configuration, cookies, origin exposure, and WebSocket policy as deployment inputs rather than component styling concerns.
## Quality Evidence Matrix
A quality gate is satisfied by observable evidence, not by the presence of a pattern in source code.
| Concern | Required evidence |
| --- | --- |
| startup and import identity | application starts through the production entry point; reload and worker behavior match deployment; no duplicate module import paths |
| initial page response | representative pages stay within their response budget or intentionally cross `client.connected()` before long work |
| interaction correctness | primary actions, keyboard submission, validation failure, retry, duplicate action, and cancellation produce deterministic state |
| client lifecycle | disconnect/reconnect within the configured window preserves valid behavior; deletion releases page-owned resources |
| concurrency | stale reads cannot overwrite newer intent; writes have a documented conflict or idempotency policy |
| blocking behavior | concurrent-client check shows one slow action does not stall unrelated page events; profiles contain no unexpected event-loop blocking |
| storage isolation | reload, navigation, second-tab, second-user, process-restart, and multi-worker checks match each selected storage scope |
| uploads | browser rejection, direct server-side rejection, oversized request, invalid content, storage failure, and successful streaming path are distinguished |
| exception behavior | expected domain failures remain actionable; unexpected failures log tracebacks and correlation IDs; controls recover from busy state |
| cache behavior | page responses are `no-store`; public assets use deliberate versioning and lifetime; protected content is not exposed through public static routes |
| responsive layout | narrow mobile, intermediate, and wide desktop viewports show no clipping, overlap, inaccessible popup content, or layout shift from dynamic labels |
| accessibility | keyboard order, focus return, accessible names, validation association, contrast, reduced-motion behavior, and dialog/menu escape behavior are verified |
| observability | logs identify operation, client/process, route or entity, timing, and outcome without secrets or sensitive payloads |
| shutdown | timers, consumers, process/thread work, persistent storage, and external clients have deliberate cancellation or close behavior |
## Testing Surfaces
NiceGUI's [pytest integration](https://nicegui.io/documentation/section_testing) provides two complementary fixtures:
- `User` simulates interactions in Python and is the fast default for page content, component values, clicks, typing, event dispatch, navigation, and service-backed acceptance behavior.
- `Screen` drives a real headless browser and is reserved for behavior that depends on browser layout, JavaScript, actual uploads/downloads, focus, WebSockets, rendering, or client-side Quasar behavior.
Use lower-level tests for services, validation, authorization, idempotency, storage adapters, and task logic without constructing UI. Use `User` tests for application interaction contracts. Use a small set of `Screen` tests for the browser boundary, and supplement responsive or visual claims with screenshots and computed layout checks at explicit viewport sizes.
Tests should control async completion by observable state, events, or bounded timeouts rather than arbitrary sleeps. Reconnect, multi-tab, multi-user, and multi-worker behavior need dedicated environments because a single simulated client cannot establish those isolation claims.
## Source Index
!!! info "NiceGUI public documentation"
- [Pages, response timeout, connection, and multicasting](https://nicegui.io/documentation/page)
- [Error handling and execution](https://nicegui.io/documentation/section_action_events)
- [Uploads](https://nicegui.io/documentation/upload)
- [Storage](https://nicegui.io/documentation/storage)
- [Navigation](https://nicegui.io/documentation/navigate)
- [Testing](https://nicegui.io/documentation/section_testing)
- [Security guidance](https://nicegui.io/documentation/section_security)
!!! info "NiceGUI `3.16.0` implementation"
- [Page response lifecycle](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/page.py)
- [Client connection and deletion](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/client.py)
- [Outbox replay](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/outbox.py)
- [Upload route](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload.py)
- [Uploaded-file buffering](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload_files.py)
- [Refreshable targets](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/functions/refreshable.py)
- [Timers](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/timer.py)
- [Storage scopes](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/storage.py)
- [Navigation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/functions/navigate.py)
- [Exception and static/media handling](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/app/app.py)
- [Static cache headers](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/staticfiles.py)
!!! info "Related platform references"
- [FastAPI WebSockets](https://fastapi.tiangolo.com/advanced/websockets/)
- [FastAPI server-sent events](https://fastapi.tiangolo.com/tutorial/server-sent-events/)
- [Starlette static files](https://www.starlette.io/staticfiles/)
- [Uvicorn deployment](https://www.uvicorn.org/deployment/)