table customization

This commit is contained in:
John Lancaster
2026-09-01 22:54:56 -05:00
parent be579c347e
commit bf11b7865d
5 changed files with 467 additions and 4 deletions
@@ -25,6 +25,7 @@ Use this skill to choose the smallest supporting reference for a NiceGUI task. T
| 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. | | 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. | | 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 or component state with `binding.bindable_dataclass`, return a bound component handle, understand propagation and transform direction, bind nested values, avoid active-link polling, or design projection/persistence rollback | [binding dataclasses](./references/binding-dataclasses.md) | Add [application architecture](./references/architecture.md) for the render-factory and thin-page boundary or [component mechanics](./references/component-mechanics.md) for browser-originated proposals. | | Model typed page or component state with `binding.bindable_dataclass`, return a bound component handle, understand propagation and transform direction, bind nested values, avoid active-link polling, or design projection/persistence rollback | [binding dataclasses](./references/binding-dataclasses.md) | Add [application architecture](./references/architecture.md) for the render-factory and thin-page boundary or [component mechanics](./references/component-mechanics.md) for browser-originated proposals. |
| Customize `ui.table` or QTable columns, formatting, classes, props, responsive density, toolbar controls, visible columns, empty states, named slots, or frontend methods | [table customization](./references/table-customization.md) | Add [editable tables](./references/tables.md) only when cells also accept server-authoritative edits. |
| 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. | | 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 `@ui.refreshable` and `@ui.refreshable_method` component regions | [interaction patterns](./references/interaction-patterns.md) | Add [application architecture](./references/architecture.md) for the reusable component contract or [binding dataclasses](./references/binding-dataclasses.md) when state propagation itself is the problem. | | Implement uploads, form submission, SSE versus WebSockets, background jobs, duplicate-submit guards, or `@ui.refreshable` and `@ui.refreshable_method` component regions | [interaction patterns](./references/interaction-patterns.md) | Add [application architecture](./references/architecture.md) for the reusable component contract or [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. | | 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. |
@@ -35,6 +36,7 @@ Use this skill to choose the smallest supporting reference for a NiceGUI task. T
- Use [application architecture](./references/architecture.md) for module ownership, not for page geometry or low-level component behavior. - Use [application architecture](./references/architecture.md) for module ownership, not for page geometry or low-level component behavior.
- Keep page functions thin: compose page shells and returned component handles there; keep each component's element tree, bindings, callbacks, and bounded refreshes in its render factory or component object. - Keep page functions thin: compose page shells and returned component handles there; keep each component's element tree, bindings, callbacks, and bounded refreshes in its render factory or component object.
- 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 [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.
- Start read-only table presentation and QTable control work in [table customization](./references/table-customization.md); keep editable state and validation in [editable tables](./references/tables.md).
- 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. - 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. - 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. - Treat [source documentation](./references/source-documentation.md) as a source index, not as an implementation workflow.
@@ -45,6 +47,7 @@ Load an example only when its exact mechanic matches the task:
- [binding transforms](./examples/data_binding.py): `bindable_dataclass`, `ui.date`, and typed `forward`/`backward` conversion. - [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. - [select events](./examples/select_events.py): `on_change`, generic Quasar events, `update:model-value`, browser-to-Python payload forwarding, and programmatic value changes.
- [table customization](./examples/table_customization.py): column defaults, dynamic formatting and classes, QTable props, responsive density, toolbar and cell slots, filtering, visible columns, and empty states.
- [editable table](./examples/editable_table.py): dataframe-to-row state, named QTable cell slots, controlled editors, dialog-based whole-row save/cancel edits, Python validation, touched rows, and canonical row refresh. - [editable table](./examples/editable_table.py): dataframe-to-row state, named QTable cell slots, controlled editors, dialog-based whole-row save/cancel edits, Python validation, touched rows, and canonical row refresh.
## Defaults That Span References ## Defaults That Span References
@@ -55,7 +58,8 @@ Load an example only when its exact mechanic matches the task:
- Avoid blocking I/O and CPU-heavy work in the UI event loop. - Avoid blocking I/O and CPU-heavy work in the UI event loop.
- Prefer event-driven updates and explicit refreshes to unrelated polling. - 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. - 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. - Prefer NiceGUI context managers and `ui.*` elements over raw Vue templates. Keep application logic and authoritative state in Python; use minimal browser expressions only for scoped-slot values or client-only behavior, following [Python-owned slot composition](./references/component-mechanics.md#prefer-python-owned-composition).
- Use NiceGUI context managers for element structure and Tailwind for generic layout, spacing, responsive behavior, and typography. Keep Quasar classes for semantic palette roles or component-specific geometry, and use Quasar props for component behavior and density; see [combining Tailwind with Quasar utilities](./references/styling-and-customization.md#combine-tailwind-with-quasar-utilities-deliberately).
- Provide loading, success, and failure states for user-triggered work. - Provide loading, success, and failure states for user-triggered work.
- Verify component-specific behavior against the installed NiceGUI version and its bundled Quasar version. - Verify component-specific behavior against the installed NiceGUI version and its bundled Quasar version.
@@ -0,0 +1,158 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
# "nicegui==3.16.0",
# ]
# ///
from nicegui import ui
type TableValue = str | int | float
type TableRow = dict[str, TableValue]
STATUS_COLORS = {
"Ready": "positive",
"Low": "warning",
"Backorder": "negative",
}
COLUMNS = [
{
"name": "name",
"label": "Product",
"field": "name",
"required": True,
"sortable": True,
"align": "left",
"headerClasses": "bg-grey-2 text-grey-9 font-bold",
"classes": "font-medium",
"headerStyle": "width: 40%; min-width: 12rem",
"style": "width: 40%; min-width: 12rem",
},
{
"name": "category",
"label": "Category",
"field": "category",
"sortable": True,
"align": "left",
"headerStyle": "width: 9rem",
"style": "width: 9rem",
},
{
"name": "stock",
"label": "In stock",
"field": "stock",
"sortable": True,
"align": "right",
"headerStyle": "width: 7rem",
"style": "width: 7rem",
":classes": "row => row.stock < 10 ? 'text-negative font-bold' : ''",
},
{
"name": "price",
"label": "Unit price",
"field": "price",
"sortable": True,
"align": "right",
"headerStyle": "width: 8rem",
"style": "width: 8rem",
":format": "value => `$${value.toFixed(2)}`",
},
{
"name": "status",
"label": "Status",
"field": "status",
"sortable": True,
"align": "center",
"headerStyle": "width: 8rem",
"style": "width: 8rem",
"colorByValue": STATUS_COLORS,
},
]
ROWS: list[TableRow] = [
{"id": 101, "name": "Desk lamp", "category": "Lighting", "stock": 7, "price": 42.5, "status": "Low"},
{"id": 102, "name": "Task chair", "category": "Seating", "stock": 18, "price": 289.0, "status": "Ready"},
{"id": 103, "name": "Monitor arm", "category": "Hardware", "stock": 0, "price": 119.95, "status": "Backorder"},
{"id": 104, "name": "Cable tray", "category": "Hardware", "stock": 34, "price": 31.25, "status": "Ready"},
{"id": 105, "name": "Side table", "category": "Furniture", "stock": 9, "price": 164.5, "status": "Low"},
{"id": 106, "name": "Floor light", "category": "Lighting", "stock": 15, "price": 98.0, "status": "Ready"},
]
def render_table() -> ui.table:
table = ui.table(
columns=COLUMNS,
column_defaults={"headerClasses": "text-grey-8"},
rows=ROWS,
row_key="id",
pagination={
"sortBy": "stock",
"descending": True,
"rowsPerPage": 5,
},
).classes("w-full max-w-5xl")
(
table.props(
# Surface and cell layout.
"flat bordered separator=horizontal wrap-cells"
)
.props(
# Local sorting and page-size choices; zero means "all rows".
'binary-state-sort :rows-per-page-options="[5, 10, 0]"'
)
.props(
# Compact cells only below Quasar's medium breakpoint.
':dense="Quasar.Screen.lt.md"'
)
.props(
# Distinguish an empty dataset from a filter with no matches.
'no-data-label="No inventory items" no-results-label="No matching inventory items"'
)
)
with table.add_slot("top-left"), ui.row().classes("items-center gap-2"):
ui.icon("inventory_2", size="sm").classes("text-primary")
ui.label("Inventory").classes("text-xl font-medium")
ui.badge(str(len(ROWS)), color="grey-3").props("text-color=grey-9")
with table.add_slot("top-right"):
ui.input(placeholder="Search inventory").props("dense outlined clearable debounce=250").bind_value_to(
table,
"filter",
)
with table.add_slot("body-cell-status"), table.cell("status"):
ui.badge().props(':label="props.value" :color="props.col.colorByValue[props.value] ?? \'grey\'" outline')
with table.add_slot("no-data"), ui.row().classes("w-full items-center justify-center gap-2 p-6 text-grey-7"):
ui.icon("inventory_2", size="2em").props(":name=\"props.filter ? 'filter_alt_off' : 'inventory_2'\"")
ui.element("span").props(':textContent="props.message"')
optional_columns = [column for column in table.columns if not column.get("required")]
def set_visible_columns(names: list[str]) -> None:
visible = set(names)
for column in optional_columns:
hidden = column["name"] not in visible
column["classes"] = "hidden" if hidden else ""
column["headerClasses"] = "hidden" if hidden else "text-grey-8"
table.update()
ui.select(
{column["name"]: column["label"] for column in optional_columns},
value=[column["name"] for column in optional_columns],
label="Visible columns",
multiple=True,
on_change=lambda event: set_visible_columns(event.value),
).props("outlined dense options-dense").classes("w-64")
return table
if __name__ in {"__main__", "__mp_main__"}:
with ui.column().classes("w-full items-center gap-4 p-4"):
render_table()
ui.run(port=8888, reload=True)
@@ -302,6 +302,14 @@ NiceGUI creates a default slot for every element. Entering an element as a conte
These mechanics are defined by the tagged [`Element.add_slot()` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/element.py), the [`Slot` context manager](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/slot.py), and NiceGUI's [context-managed scoped-slot examples](https://github.com/zauberzeug/nicegui/blob/v3.16.0/website/documentation/content/table_documentation.py). These mechanics are defined by the tagged [`Element.add_slot()` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/element.py), the [`Slot` context manager](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/slot.py), and NiceGUI's [context-managed scoped-slot examples](https://github.com/zauberzeug/nicegui/blob/v3.16.0/website/documentation/content/table_documentation.py).
### Prefer Python-Owned Composition
Use NiceGUI context managers and `ui.*` elements for slot structure whenever they can represent the required element tree. Keep values, mappings, validation, permissions, event handling, and authoritative state transitions in Python. This preserves element identity, typed wrapper APIs, lifecycle cleanup, test visibility, and the normal NiceGUI update path.
Use the narrowest browser-side expression for state that exists only while Quasar renders a scoped slot. A dynamic prop such as `:label="props.value"` may project that value into a NiceGUI element without moving the surrounding structure or business rules into JavaScript. When Python needs a browser-owned value, emit the smallest serializable proposal to a Python handler and validate it there.
Escalate to `add_slot(name, template)` only when the slot contract requires client-side structure that context-managed NiceGUI elements cannot preserve, such as a browser-side `v-for`, a variable number of sibling roots, or Vue's object form of `v-bind` for a Quasar interaction bundle. Keep raw templates small, use documented scoped props, and do not duplicate authoritative application logic in JavaScript.
### Context-Managed NiceGUI Elements ### Context-Managed NiceGUI Elements
Ordinary NiceGUI elements can populate slot content: Ordinary NiceGUI elements can populate slot content:
@@ -313,7 +321,7 @@ with name_input.add_slot("prepend"):
ui.icon("person") ui.icon("person")
``` ```
Nested context managers express the component hierarchy while preserving NiceGUI element identity, event registration, updates, deletion, and test visibility. The `add_slot(name, template)` form accepts a raw Vue template for client-side structures such as a `v-for` that creates a variable number of sibling elements. Nested context managers express the component hierarchy while preserving NiceGUI element identity, event registration, updates, deletion, and test visibility.
### Scoped Props On The Client ### Scoped Props On The Client
@@ -82,7 +82,20 @@ Tailwind's [responsive variants](https://tailwindcss.com/docs/responsive-design)
### Combine Tailwind With Quasar Utilities Deliberately ### Combine Tailwind With Quasar Utilities Deliberately
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: NiceGUI's `.classes()` accepts both Tailwind utilities and CSS helpers bundled with Quasar. Assign each concern to one system:
| Concern | Default owner | Examples |
| --- | --- | --- |
| Python element structure | NiceGUI | `ui.row`, `ui.column`, `ui.grid`, slot context managers |
| Generic geometry and responsive layout | Tailwind | `w-full`, `items-center`, `justify-center`, `gap-2`, `p-6`, `flex-wrap` |
| Generic application typography | Tailwind | `text-xl`, `font-medium`, `leading-6`, `truncate` |
| Theme-aware semantic color | Quasar | `text-primary`, `bg-positive`, `text-grey-7` |
| Component behavior and density | Quasar props | `dense`, `outlined`, `round`, `separator=horizontal` |
| Component-specific geometry | Quasar helper classes | `q-table--col-auto-width`, `absolute-top-right` |
Prefer the Tailwind spelling when both systems express ordinary application layout or typography. For example, use `w-full` instead of `full-width`, `items-center justify-center` instead of `flex-center`, `gap-2` instead of `q-gutter-sm`, `p-4` instead of `q-pa-md`, and `text-xl font-medium` instead of `text-h6 text-weight-medium`. This keeps spacing, breakpoints, and type choices in one vocabulary.
Quasar helpers remain useful when a value intentionally follows Quasar's component conventions:
- [`q-m*` and `q-p*` spacing classes](https://quasar.dev/style/spacing) when spacing should follow Quasar's component scale - [`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 - [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
@@ -90,7 +103,7 @@ NiceGUI's `.classes()` accepts both Tailwind utilities and CSS helpers bundled w
- [positioning helpers](https://quasar.dev/style/positioning), such as `absolute-top-right`, when positioning content relative to a Quasar component - [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 - [size and overflow helpers](https://quasar.dev/style/other-helper-classes), such as `fit`, `full-width`, and `overflow-auto`, when matching Quasar layout behavior
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. Do not assign the same property through both systems on one element. For example, `w-full q-pa-md` deliberately 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 ```python
with ui.card().classes("w-full max-w-2xl q-pa-md"): with ui.card().classes("w-full max-w-2xl q-pa-md"):
@@ -0,0 +1,280 @@
# Table Customization
Use this reference when a [`ui.table`](https://nicegui.io/documentation/table) needs presentation, controls, responsive behavior, or custom cell content while retaining [Quasar QTable](https://quasar.dev/vue-components/table) sorting, filtering, pagination, and selection behavior. Use [editable tables](./tables.md) instead when browser-originated cell values must be validated and committed by Python.
## Version Baseline
This reference and its runnable example were verified against this bundled stack:
| Layer | Version | Evidence |
| --- | --- | --- |
| NiceGUI | `3.16.0` | [Tagged `Table` source](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/table.py) |
| Quasar | `2.18.5` | [NiceGUI frontend dependencies](https://github.com/zauberzeug/nicegui/blob/v3.16.0/package.json) |
| Vue | `3.5.22` | [NiceGUI frontend dependencies](https://github.com/zauberzeug/nicegui/blob/v3.16.0/package.json) |
Recheck the tagged NiceGUI sources and bundled dependencies for another release. The current Quasar documentation may describe features added after NiceGUI's bundled Quasar version.
## Choose The Narrowest Layer
Apply customization at the highest-level API that expresses it:
1. Use `ui.table(...)` for rows, columns, defaults, stable row identity, title, selection, and pagination.
2. Use column definitions for alignment, sorting, formatting, cell classes, header classes, and width hints.
3. Use `.props(...)` for QTable behavior that the NiceGUI constructor does not expose, such as `dense`, `separator`, `wrap-cells`, `rows-per-page-options`, and empty-state labels.
4. Use named slots for custom toolbar content, one special header or cell, loading, no-data content, or pagination controls.
5. Use a full `header`, `body`, or `item` slot only when the whole generated structure must change.
6. Add narrowly scoped CSS for behavior that neither component API covers, such as sticky columns.
NiceGUI's tagged [table client wrapper](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/table.js) passes props to QTable and forwards every supplied slot with its scoped properties. This makes QTable's API the source of truth below the NiceGUI wrapper, but it does not make every current QTable feature compatible with the bundled `2.18.5` release.
## Columns Before Slots
A QTable column is both a data projection and a presentation contract. Keep its `name` unique because sorting and `body-cell-[name]` slot selection use it. Keep `field` separate when the displayed column name differs from the row key.
```python
columns = [
{
"name": "price",
"label": "Unit price",
"field": "price",
"sortable": True,
"align": "right",
":format": "value => `$${value.toFixed(2)}`",
},
]
table = ui.table(
rows=rows,
columns=columns,
column_defaults={"headerClasses": "text-grey-8"},
row_key="id",
)
```
Use plain keys such as `classes`, `style`, `headerClasses`, and `headerStyle` for static values. NiceGUI's client-side dynamic-property conversion recognizes colon-prefixed keys such as `:field`, `:format`, `:sort`, `:classes`, and `:style` as JavaScript expressions. Keep row data JSON-serializable; format display values in a column or slot rather than putting component objects in rows.
### Cell Classes And Styles
Quasar's tagged [`table-column-selection.js`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/table-column-selection.js), [`QTh.js`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/QTh.js), and [`QTd.js`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/QTd.js) establish the exact targets:
| Column key | Applied to | Static or dynamic | Typical uses |
| --- | --- | --- | --- |
| `headerClasses` | The generated `<th>` for this column | Static string | Header color, weight, whitespace, sticky positioning, utility-based width |
| `classes` | Every generated `<td>` in this column | String or function of `row` | Body typography, whitespace, conditional color, utility-based width |
| `headerStyle` | Inline `style` on the generated `<th>` | Static string | Header-specific dimensions or positioning |
| `style` | Inline `style` on every generated `<td>` | String or function of `row` | Body dimensions, overflow, or row-dependent presentation |
`align` is separate: Quasar prepends `text-left`, `text-right`, or `text-center` to both header and body classes. It also appends header state classes such as `sortable`, `sorted`, and `sort-desc`. Supplying `classes` or `headerClasses` does not remove those generated classes.
These fields interact through normal CSS rules:
- On the same cell, inline `style` normally wins over a conflicting class declaration. A class rule containing `!important` can beat a normal inline declaration; avoid building a width policy around that exception.
- The order of class names in the `class` attribute does not decide precedence. CSS origin, importance, cascade layer, selector specificity, and stylesheet source order do.
- `headerClasses` never flows into body cells, and `classes` never flows into the header. The same separation applies to `headerStyle` and `style`.
- `column_defaults` is merged as `{**defaults, **column}`. A column-level value replaces the complete default value for that key; class strings are not concatenated. Include shared classes again in an overriding column value when they must be retained.
- A named `header-cell-*` or `body-cell-*` slot replaces QTable's default cell renderer. Use `table.header(column_name)` or `table.cell(column_name)` so Quasar reapplies the computed column classes and styles to the resulting `QTh` or `QTd`.
Prefer classes for reusable visual policy and static utility classes. Prefer `headerStyle` and `style` for one-off values, especially dimensions that do not have a clear project utility. Use `:classes` or `:style` only when the value genuinely depends on the browser-side row; otherwise a static value is easier to inspect and override.
### Column Width Model
QTable renders a native table with `width: 100%`, `max-width: 100%`, and, by default, the browser's [`table-layout: auto`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/table-layout) algorithm. One column is a shared track: its header and all body cells receive the same final used width even though each cell can contribute a different width constraint.
This has several consequences:
1. A `width` in `headerStyle` and a different `width` in `style` do not override each other because they are declarations on different elements. The browser considers both, along with every cell's min-content and max-content size, and computes one column width.
2. Under automatic layout, `width` is a strong sizing input, not a guaranteed cap. Long unbreakable content, cell padding, other columns, and the table's available width can make the column wider.
3. `min-width` supplies a floor. `max-width` alone is not a dependable truncation mechanism for an automatic table because intrinsic content still participates in track sizing.
4. QTable is `nowrap` by default through `q-table--no-wrap`. The `wrap-cells` prop removes that rule, allowing ordinary wrapping and reducing columns toward their min-content widths. For IDs or URLs, add [`overflow-wrap: anywhere`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/overflow-wrap) to create breaks inside otherwise unbreakable strings.
5. Cell padding contributes to the track width. QTable uses `16px` horizontal padding per side normally and `8px` in dense mode, with larger edge padding retained for the first and last columns.
Use the following mechanisms in order:
| Goal | Recommended mechanism |
| --- | --- |
| Let content choose sensible widths | Leave `style` and `headerStyle` unset; keep automatic layout |
| Keep a column from becoming too narrow | Put the same absolute `min-width` in `style` and `headerStyle` |
| Give columns proportional targets | Put matching percentage `width` values in `style` and `headerStyle`; treat them as targets under automatic layout |
| Keep utility or action cells compact | Use `auto-width` on `table.header(...)` and `table.cell(...)` in slots, or Quasar's `q-table--col-auto-width` class on both header and body; it sets `width: 1px` and content supplies the floor |
| Allow readable narrow layouts | Enable `wrap-cells`, set a practical `min-width` for key columns, and let QTable's middle container scroll horizontally when the total minimum exceeds the viewport |
| Guarantee a column allocation | Use fixed table layout, an explicit table width, matching first-row/header widths, and an explicit overflow policy |
| Truncate text | Use a fixed/constrained track plus a block wrapper with `overflow: hidden; white-space: nowrap; text-overflow: ellipsis`; [`text-overflow`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/text-overflow) does not create overflow by itself |
For ordinary data tables, automatic layout plus a few minimum or target widths is the idiomatic default. Apply matching values to `headerStyle` and `style`: one declaration is often enough to influence the shared track, but matching declarations make intent explicit, survive empty datasets, and keep custom header/body renderers consistent.
Use fixed layout only when predictable allocation matters more than content-driven sizing. QTable's `table-style` prop styles the scrolling wrapper `<div>`, not the nested native `<table>`, so it cannot set `table-layout`. Add a class to the `ui.table` element and target the nested `.q-table` with scoped CSS:
```python
ui.add_css("""
.inventory-table .q-table {
table-layout: fixed;
width: 100%;
}
.inventory-table .truncate-cell > * {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
""")
table = ui.table(rows=rows, columns=columns).classes("inventory-table w-full")
```
With fixed layout, the table must have a non-automatic width. The first row's explicit widths determine tracks without a `<colgroup>`; later body content does not resize them and therefore needs wrapping, clipping, or scrolling. A full custom header changes that first-row contract, so retest every column after introducing one.
Column visibility has two useful patterns:
- Mark identity or action columns `required` when they must remain visible.
- For a Python-owned column picker, update each optional column in `table.columns`, then call `table.update()`. Use the table-owned dictionaries rather than the input list because `column_defaults` normalizes columns into new dictionaries. This pattern also avoids binding a Python list directly into a JavaScript expression.
## QTable Props
Pass static QTable props as whitespace-delimited tokens. Prefix a prop with `:` only when its value is a JavaScript expression:
```python
table.props(
'flat bordered separator=horizontal wrap-cells '
':dense="Quasar.Screen.lt.md" '
':rows-per-page-options="[5, 10, 0]"'
)
```
Use static props for fixed component policy and dynamic props for small browser-owned presentation decisions such as responsive density. Keep authoritative business state and permission decisions in Python.
Useful QTable presentation props include:
| Need | Props |
| --- | --- |
| Surface | `flat`, `bordered`, `square`, `dark`, `color` |
| Cell layout | `dense`, `separator`, `wrap-cells` |
| Layers | `hide-header`, `hide-bottom`, `hide-pagination`, `hide-no-data` |
| Labels | `no-data-label`, `no-results-label`, `loading-label`, `rows-per-page-label` |
| Paging and sorting | `rows-per-page-options`, `binary-state-sort`, `column-sort-order` |
| Large local datasets | `virtual-scroll`, `virtual-scroll-item-size`, `virtual-scroll-sticky-size-start` |
Virtual scrolling needs a bounded height and accurate row-size assumptions. If a full `body` slot renders multiple `QTr` elements for one data row, follow Quasar's `q-virtual-scroll--with-prev` and unique-key requirements. Do not enable virtual scrolling as a default for a small table.
## Named Slots
Prefer the smallest QTable slot that owns the customization:
| Slot | Use |
| --- | --- |
| `top-left`, `top-right` | Title, filters, column controls, export |
| `header-cell-[name]` | One custom header while preserving other generated headers |
| `body-cell-[name]` | One custom cell type while preserving generated rows and other cells |
| `no-data`, `loading` | Empty, filtered-empty, and busy states |
| `pagination` | Custom page controls |
| `footer` | A real table footer such as totals |
Since NiceGUI `3.5.0`, a scoped slot can contain NiceGUI elements. Follow the [Python-owned slot composition](./component-mechanics.md#prefer-python-owned-composition) rule: use context managers and NiceGUI elements for structure, keep application logic in Python, and reserve dynamic props for scoped values that exist only in the browser. Wrap body-cell content in `table.cell(column_name)` so QTable retains the column's alignment and cell semantics:
```python
STATUS_COLORS = {
"Ready": "positive",
"Low": "warning",
"Backorder": "negative",
}
columns = [
{
"name": "status",
"label": "Status",
"field": "status",
"colorByValue": STATUS_COLORS,
},
]
with table.add_slot("body-cell-status"), table.cell("status"):
ui.badge().props(
':label="props.value" '
':color="props.col.colorByValue[props.value] ?? \'grey\'"'
)
```
Inside table slots, `props.value` is the parsed and formatted cell value, `props.row` is the row object, and `props.col` is the column definition. Custom JSON-serializable column keys such as `colorByValue` therefore provide a clean bridge from Python-owned display policy to a reused browser-side slot. Use Quasar color names in the mapping when the component's `color` prop should follow the active theme, and include a fallback for unexpected values.
A slot template is reused for every matching row, so `props.value`, `props.row`, and `props.col` are JavaScript expressions, not Python variables. Keep authorization and business-state decisions out of this mapping; it is client-visible presentation metadata.
Replacing the full `body` or `header` slot also replaces behavior QTable would otherwise generate. Render `QTr` plus `QTd` or `QTh`, pass the scoped props through, preserve unique row keys, and retest sorting, selection, focus, and responsive behavior. In particular, QTable row-click events are not emitted when a full `body`, `row`, or `item` slot owns the structure.
## Toolbar Search
NiceGUI controls can live in QTable toolbar slots. Bind an input's value one way to the table's `filter` property for a small, local dataset:
```python
with table.add_slot("top-right"):
ui.input(placeholder="Search inventory").props(
"dense outlined clearable debounce=250"
).bind_value_to(table, "filter")
```
The path from keystroke to displayed rows is:
1. `debounce=250` waits until input has been idle for 250 milliseconds, reducing value-change traffic while the user types.
2. `bind_value_to(table, "filter")` immediately initializes `table.filter` from the input and then propagates later input values in that direction only.
3. For local rows, QTable lowercases the search term and each computed cell value. A row remains when at least one column value contains the term as a substring.
4. QTable filters first, sorts the matching rows, resets pagination to page 1 when the filter changes, and then slices the current page.
5. When no row matches, the `no-data` slot receives the configured `no-results-label` as `message` and a truthy `filter`; when the underlying row list is empty without a filter, it receives `no-data-label` and a falsy `filter`.
The default matcher uses each column's resolved `field` and then its `format` function. In this example, searches can therefore match displayed values such as `Hardware`, `Backorder`, or `$31.25`; the row ID is not searchable because it has no column. QTable passes its computed columns to the matcher. A column excluded with QTable's `visible-columns` prop is not searched, while a column hidden only with CSS classes, as in this example's Python-owned picker, remains part of the search.
This is client-side filtering over rows already sent to the browser. Do not load a large dataset merely to search it locally. When pagination contains `rowsNumber`, QTable switches to server-side mode, stops applying its local matcher, and emits a `request` carrying the filter and pagination state; validate the term and query the authoritative data source in that handler. Add explicit backend search semantics for field scope, tokenization, locale, and ranking instead of assuming they match QTable's substring behavior.
When Python needs the browser's current result set, prefer `await table.get_filtered_sorted_rows()` for all matches before pagination or `await table.get_computed_rows()` for the current page. These helpers require a connected client and should not replace backend filtering for server-owned data.
Prefer typed table helpers over raw frontend calls. Use `table.run_method(...)` only for QTable methods without a NiceGUI helper, such as `scrollTo`, `sort`, or `firstPage`, and verify the method in the bundled QTable API first.
## Runnable Example
The complete example combines column defaults, static and dynamic column attributes, responsive QTable props, filtering, column visibility, a toolbar, a custom status cell, and a filtered-empty state. It is available as [`table_customization.py`](../examples/table_customization.py) and as `skill://nicegui/examples/table_customization.py`.
```python title="table_customization.py"
--8<-- "docs/skills/nicegui/examples/table_customization.py"
```
Run it from the repository root:
```bash
uv run src/personal_mcp/docs/skills/nicegui/examples/table_customization.py
```
Verify that searches are case-insensitive, match formatted values across columns, reset to the first page, and change the empty-state message when no rows match. Confirm that optional columns can be hidden and restored and that the status cell retains its alignment and badge styling. Resize the browser across mobile, landscape desktop, and portrait desktop widths; the toolbar must remain usable and the table must scroll without overlapping controls.
## Escalation Boundaries
- For editable cells, stable row identity, proposal validation, and canonical refresh, use [editable tables](./tables.md).
- For generic scoped-slot values, event forwarding, and model events, use [component mechanics](./component-mechanics.md).
- For page width, overflow, typography, and static CSS loading, use [page structure, typography, and scaling](./styling-and-customization.md).
- For theme colors and dark mode, use [NiceGUI and Quasar color theming](./colors-and-quasar-theming.md).
## Source Index
!!! info "Primary documentation"
- [NiceGUI table documentation](https://nicegui.io/documentation/table)
- [Quasar QTable documentation](https://quasar.dev/vue-components/table)
- [CSS table layout algorithm](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/table-layout)
- [CSS width sizing](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/width)
!!! info "NiceGUI `3.16.0` implementation"
- [`Table` Python source](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/table.py)
- [Table client wrapper](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/table.js)
- [Frontend dependency manifest](https://github.com/zauberzeug/nicegui/blob/v3.16.0/package.json)
!!! info "Bundled Quasar `2.18.5` implementation"
- [QTable API definition](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/QTable.json)
- [QTable implementation](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/QTable.js)
- [QTable column computation](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/table-column-selection.js)
- [QTable styles](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/QTable.sass)
## Completion Check
Before accepting a customized table:
1. Verify the target NiceGUI release and its bundled Quasar version.
2. Set a primitive, immutable, unique `row_key` before using selection or row-scoped actions.
3. Use constructor arguments and column definitions before QTable props or slots.
4. Use `:` only for JavaScript expressions and keep row values serializable.
5. Choose the narrowest named slot and preserve QTable cell or header semantics.
6. Recheck sorting, filtering, pagination, selection, and empty states after customization.
7. Check mobile, landscape desktop, and portrait desktop layouts; verify the toolbar remains usable and wide tables scroll without overlapping controls.