# 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. `field` identifies or computes the raw cell value; it does not need to match the column name. ```python columns = [ { "name": "available", "label": "In stock", "field": "stock", "sortable": True, "align": "right", }, ] 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. ### Raw Values And Cosmetic Display Formatting Use the column's `format(value, row)` function when only the displayed cell text should change. QTable's tagged [`getCellValue`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/QTable.js) resolves `field` first and then passes that raw value through `format`. The formatted result is used by the default cell renderer and exposed to `body-cell-*` slots as `props.value`. Keep the layers distinct: | Layer | Owns | Effect | | --- | --- | --- | | row value | canonical JSON-serializable data sent by Python | remains available as `props.row.` | | `field` | raw value extraction or derivation | supplies sorting and the input to `format` | | `format` | cosmetic text projection | changes default rendering, `props.value`, and local default-filter matching | | `body-cell-*` slot | component structure around the value | use for badges, icons, links, controls, or multiple elements | Formatting does not mutate `table.rows` or the row object. QTable's tagged [sorting implementation](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/table-sort.js) compares raw `field` values, while its tagged [default filter](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/table-filter.js) searches the formatted values returned by `getCellValue`. This is usually desirable: numbers sort numerically while users can search what they see. #### Prefix And Suffix Text Use a null-safe `:format` expression for simple prefix or suffix text: ```python columns = [ { "name": "price", "label": "Unit price", "field": "price", "sortable": True, ":format": "value => value == null ? '' : `$${value.toFixed(2)}`", }, { "name": "stock", "label": "In stock", "field": "stock", "sortable": True, ":format": "value => value == null ? '' : `${value} units`", }, ] ``` The underlying values stay numeric, so sorting remains numeric. Check `value == null` rather than `if (!value)` when zero is valid; `0` must render as `$0.00` or `0 units`, not as an empty cell. Use a named cell slot instead when the prefix or suffix needs separate styling, an icon, a tooltip, or accessible text that differs from the visible text. #### Datetime Text Send datetimes as ISO 8601 strings with an explicit offset. Python's [`datetime.isoformat()`](https://docs.python.org/3/library/datetime.html#datetime.datetime.isoformat) includes an offset for aware values, and [`astimezone()`](https://docs.python.org/3/library/datetime.html#datetime.datetime.astimezone) preserves the represented instant while converting zones: ```python from datetime import UTC, datetime updated_at = datetime.now(UTC) row = {"updated_at": updated_at.astimezone(UTC).isoformat()} ``` Avoid locale-formatted strings and naive datetime strings as transport values. JavaScript guarantees support for its standard ISO date-time format, but [date-time strings without an offset](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse) are interpreted in the browser's local timezone when they contain both a date and time. An explicit `Z` or `+00:00` identifies the instant unambiguously. For repeated cells, construct one [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) and return a formatter closure. NiceGUI evaluates the colon-prefixed expression into the column function, so the formatter is reused instead of performing locale-data lookup for every cell: ```python columns = [ { "name": "updated_at", "label": "Updated", "field": "updated_at", "sortable": True, ":sort": "(left, right) => Date.parse(left) - Date.parse(right)", ":format": """(() => { const formatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeStyle: 'short', timeZone: 'UTC', }); return value => { if (!value) return ''; const timestamp = Date.parse(value); return Number.isNaN(timestamp) ? 'Invalid date' : formatter.format(timestamp); }; })()""", }, ] ``` Choose locale and timezone deliberately: - Use a fixed locale such as `en-US` when the product requires one stable language convention; use `undefined` to follow the browser's locale. - Set `timeZone` to `UTC` or an IANA zone such as `America/New_York` for deterministic product behavior; omit it only when each viewer should see browser-local time. - Keep invalid-value handling explicit. `Date.parse()` returns `NaN` for an invalid value, while passing an invalid date directly to the formatter can throw. Sorting still receives the raw ISO value from `field`; it never compares the formatted label. The explicit `:sort` function above converts those raw strings to epoch milliseconds, so values with different offsets are ordered by instant while cells keep their localized display text. Validate datetime strings before sending them because an invalid value makes `Date.parse()` return `NaN`. For a simpler data contract, send epoch milliseconds as the field value and format that number directly. Lexicographic sorting without a custom `sort` function is chronological only when every ISO string uses the same fixed-width representation and offset, as in normalized UTC values. Local filtering is intentionally different: it matches the formatted text, so searches follow the chosen locale and timezone rather than the raw ISO string. ### 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 `` for this column | Static string | Header color, weight, whitespace, sticky positioning, utility-based width | | `classes` | Every generated `` in this column | String or function of `row` | Body typography, whitespace, conditional color, utility-based width | | `headerStyle` | Inline `style` on the generated `` | Static string | Header-specific dimensions or positioning | | `style` | Inline `style` on every generated `` | 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 Tailwind's `truncate` utility; [`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 `
`, not the nested native ``, so it cannot set `table-layout`. This nested element cannot receive a utility class through NiceGUI or QTable's public API, making one application class and one scoped CSS rule necessary. Keep truncation on a Python-created wrapper with Tailwind's `truncate` utility: ```python ui.add_css(""" .inventory-table .q-table { table-layout: fixed; } """) table = ui.table(rows=rows, columns=columns).classes("inventory-table w-full") with table.add_slot("body-cell-name"), table.cell("name"): ui.element("span").props(':textContent="props.value"').classes("block truncate") ``` With fixed layout, the table must have a non-automatic width; QTable already gives its native table `width: 100%`, while `w-full` constrains the NiceGUI element. The first row's explicit widths determine tracks without a ``; 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, assign the selected column names to `table.props["visible-columns"]`, then call `table.update()`. QTable automatically includes columns marked `required`. Assign the Python list directly to the prop rather than interpolating it 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. ### Add A Button To A Cell Add an action column to the table's column definitions, then target it with the corresponding `body-cell-[name]` slot. Use a NiceGUI `ui.button` rather than writing a raw QBtn template, and wrap it in `table.cell(column_name)` so QTable preserves the cell's alignment, classes, styles, and table semantics. The slot's `props.key` is the primitive identity derived from the table's `row_key`. Forward that key to Python instead of sending the full browser-side row object. Resolve the current authoritative row again in the handler because the record may have changed or disappeared since the table was rendered: ```python from nicegui import events from nicegui import ui columns = [ {"name": "actions", "label": "Actions", "required": True, "align": "center"}, {"name": "name", "label": "Product", "field": "name", "align": "left"}, ] rows = [ {"id": 101, "name": "Desk lamp"}, {"id": 102, "name": "Task chair"}, ] table = ui.table(rows=rows, columns=columns, row_key="id") def open_product(event: events.GenericEventArguments) -> None: product = next((row for row in table.rows if row[table.row_key] == event.args), None) if product is None: ui.notify("Product no longer exists", type="negative") return ui.notify(f"Opening {product['name']}") with ( table.add_slot("body-cell-actions"), table.cell("actions"), ui.button(icon="open_in_new") .props("round flat size='sm'") .on( "click.stop", handler=open_product, js_handler="() => emit(props.key)", ), ): ui.tooltip("Open product") ``` The compact icon-only button uses QBtn's `round`, `flat`, and `size` props to avoid a visually heavy filled action in every row. Nest `ui.tooltip` inside the button so each row's cloned QTooltip uses its own parent as the target. Do not call `action_button.tooltip(...)` in a reused scoped slot: NiceGUI implements that convenience method with an element-ID target, and every clone would resolve to the first button. The `.on(...)` bridge is necessary here because `props.key` exists only in the reused browser-side slot scope; a normal Python `on_click` callback cannot capture a different row for each rendered instance. The `click.stop` modifier prevents the button click from also reaching a row-click handler. Treat the key as untrusted input and repeat authorization, existence, and state checks before performing the real action. 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, so a column excluded with the `visible-columns` prop is not searched. Hiding a column with CSS classes would leave it in the search set and should not be used as a substitute for the component prop. 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 raw-value sorting with cosmetic prefix, suffix, and datetime formatting; column defaults; responsive QTable props; filtering; column visibility; a toolbar; custom status and action cells; 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 stock and price sort by their numeric row values while displaying suffix and prefix text. Verify that updated timestamps sort chronologically by their raw ISO values while displaying localized UTC text. Confirm 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, the status cell retains its alignment and badge styling, and each action button reports the product from its own row. Hover action buttons in multiple rows and confirm that each one independently shows exactly one `Open product` tooltip. 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) - [Python aware and naive datetimes](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects) - [JavaScript `Date.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse) - [JavaScript `Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) - [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. Keep sortable values canonical and serializable; use `format` only for cosmetic text and `sort` only when raw values need a custom comparator. 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.