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
@@ -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.