Data Table
FreeSortable, paginated table with row selection, badge cells and a loading skeleton — plus premium toolbar, column filters, inline editing, expandable rows and bulk actions.
Complexity tier: Application The interactive bar, plus a stated data-volume ceiling, a persistence contract, a bundle budget and an accessible parity surface.
import { SvDataTableComponent } from '@styloviz/data-table';Demo & usage
Basic usage
Usage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Searchable toolbar + bulk actions
<sv-data-table [rows]="rows" rowKey="id" [searchable]="true" [showToolbar]="true" [selectable]="true" [bulkActions]="bulkActions" [pageSize]="20" (rowClick)="open($event)" />
Server-side paging
<sv-data-table [rows]="pageRows" [serverSide]="true" [totalCount]="total" (serverPageChange)="load($event)" />
Transparent — inherit the container background (Pro)
<!-- Drops the table's own white fill so it blends into a glass card or coloured panel; keeps hover/selection tints. Pair with variant="borderless" so only the parent card's border shows. --> <div class="rounded-2xl border bg-white/70 backdrop-blur-xl p-1"> <sv-data-table-pro [columns]="columns" [rows]="rows" rowKey="id" [transparent]="true" variant="borderless" [paginated]="false" /> </div>
Custom cell templates (svDtCell)
<sv-data-table [columns]="columns" [rows]="rows" rowKey="id"> <!-- Render any rich content per column, keyed by the column's `key`. --> <ng-template svDtCell="name" let-row let-value="value"> <sv-avatar [name]="row.name" size="xs" /> {{ value }} </ng-template> <ng-template svDtCell="status" let-row> <sv-badge [label]="row.status" [variant]="statusVariant(row.status)" /> </ng-template> </sv-data-table>
Compare products, transposed (Pro)
<!-- Transposes rows→columns: first column = product name, the rest become feature rows; boolean values render as check / cross. --> <sv-data-table-pro [columns]="columns" [rows]="products" rowKey="id" layout="comparison" highlightRowId="pro" />
When to use
- Displaying structured rows that users sort, page, search, or select.
- You need selection, bulk actions, density toggles or column visibility.
- For unstructured/feed-style content, use List View instead.
- Up to ~200 rows: render them unpaged. Every row is in the DOM, so nothing needs configuring.
- 200 rows to ~10k, held in the browser: `paginated` (or `virtualized` for one continuous scroll). The client pipeline — search, filter, sort and windowing over the whole set — measures 0.5ms at 1k rows and 4.4ms at 10k, so a keystroke still lands inside one frame.
- 10k to ~25k: `virtualized` keeps rendering flat (the DOM holds `viewportHeight / virtualRowHeight` rows plus a small overscan no matter how large the set is — a 240-row table renders 13 `tr`s), but the pipeline is linear in row count and search is what runs it. Turn `searchable` off, or move to `serverSide`.
- Beyond ~25k rows: `serverSide`. The pipeline costs 21ms at 50k rows, so each keystroke drops a frame — and that is before the memory of holding the set client-side.
Async states
- loading
- empty
- error
- retry
- partial
Item identity & owned state
- Item key
rowKey— set this when your rows have noid, so selection survives a reorder.- Restorable state
- expandedRowIds
- hiddenColumnKeys
- revealedColumnKeys
- selectedIds
- selectionChange
Bind these to put a saved view back — the open sections, the hidden columns, the selected rows — and read their
Changeevents to store what the user does.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| columns | TableColumn<T>[] | [] | Column definitions (key, header, alignment, optional cell template). |
| rows | readonly T[] | [] | The rows to render. `readonly` on purpose: the table never mutates this array, and anything that hands it one — a signal, a `computed`, `svDataController.items` — is handing over a value it still owns. A mutable `T[]` here rejected every one of those at the template boundary while promising a write the table does not perform. |
| rowKey | SvItemKey<T> | 'id' | What identifies a row: a property name, or a function for composite keys. Widened from `string` because a row identified by two fields — an order line keyed on `orderId` + `lineNo`, a matrix cell on `row` + `col` — had no way to say so, and the fallback silently keyed it on the whole row's JSON. See the identity contract on `resolveItemKey` in `@styloviz/core`. |
| selectable | boolean | false | Enable row selection. |
| selectMode | SelectMode | 'multi' | 'multi' = checkboxes, 'single' = radio buttons. |
| selectedIds | Set<string | number> (two-way) | new Set() | Selected row ids (two-way). |
| expandedRowIds | Set<string | number> (two-way) | new Set() | Which rows have their detail panel open (two-way). A `model` rather than internal state because it is the same kind of fact as `selectedIds`: a host restoring a table after a route change, or opening the row a deep link names, has to be able to write it — and one that persists layout has to be able to see it change. Held in a plain signal it was neither settable nor observable from outside. |
| actions | TableRowAction<T>[] | [] | Per-row action buttons. |
| bulkActions | BulkAction[] | [] | Bulk actions applied to selected rows. |
| loading | boolean | false | Show a loading state. |
| loadingStyle | LoadingStyle | 'skeleton' | 'skeleton' rows or full-table 'overlay' spinner. |
| skeletonRows | number | 5 | Number of skeleton rows while loading. |
| paginated | boolean | true | Enable client-side pagination. |
| pageSize | number (two-way) | 10 | Rows per page (two-way). |
| pageSizeOptions | number[] | [10, 25, 50] | Page-size options offered in the toolbar. |
| serverSide | boolean | false | Server-side mode: disables client filtering/sorting/paging. |
| totalCount | number | null | null | Total row count when in server-side mode. |
| variant | TableVariant | 'default' | Table style variant. |
| density | TableDensity | 'comfortable' | Row density: comfortable, compact, etc. |
| compact | boolean | false | Deprecated shorthand for compact density. |
| maxHeight | string | null | null | Max body height (CSS length) before the table scrolls. |
| showScrollHint | boolean | true | Show a hint when the table is horizontally scrollable. |
| rowClassFn | ((row: T) => string) | null | null | Callback returning extra Tailwind classes for a given row. |
| showToolbar | boolean | undefined | undefined | Show the toolbar strip. Left unset it DERIVES from whether the toolbar has anything to put in it — binding `tableTitle` or `[searchable]` used to render nothing at all unless you also remembered this flag, which is a silent no-op: the API accepted your input and ignored it. Set it explicitly to force the strip on (an empty bar as a spacer) or off (a title used only as the accessible name). Every existing call site passes it, so nothing changes for them. |
| tableTitle | string | '' | Title shown in the toolbar. |
| searchable | boolean | false | Show a search box in the toolbar. |
| showColumnToggle | boolean | false | Show the column visibility toggle. |
| exportable | boolean | false | Show an export (CSV) control. |
| showDensityToggle | boolean | false | Show the density toggle. |
| columnFilters | boolean | false | Enable per-column filters. |
| multiSort | boolean | false | Allow sorting by multiple columns. |
| expandable | boolean | false | Enable expandable rows. |
| expandRender | ((row: T) => string) | null | null | Renderer for the expanded row detail. |
| showFooter | boolean | false | Show a table footer row. |
| cardOnMobile | boolean | false | Render rows as cards on small screens. |
| emptyMessage | string | 'No data to display.' | Message shown when there are no rows. |
| customClass | string | '' | Extra CSS classes for the root element. |
| error | SvDataError | string | null | null | Load failure. Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`. Rendered in place of the rows, above the toolbar so the user can still change the filter that caused it. |
| hiddenColumnKeys | Set<string> (two-way) | new Set() | Which columns the user has hidden (two-way). A `model` rather than internal state so a saved table layout can be restored by binding it — the free tier had no other way in at all, and the Pro tier could only reach it through a whole-state snapshot. Declared here with the rest of the internal view state because that is where the column toggle lives; it is nonetheless part of the public API. |
| revealedColumnKeys | ReadonlySet<string> (two-way) | new Set() | Columns declared `hidden: true` that the user has since shown from the Columns menu. `hidden` is documented as "initially hidden — the user can show it from the column-toggle panel", but the table used to filter `!col.hidden` unconditionally: the column could never come back, and its checkbox (bound to `hiddenColumnKeys` alone) sat TICKED for a column that was not on screen. Two-way, like `hiddenColumnKeys`: together the two ARE the column-visibility view, and a host that persists one without the other restores a table whose revealed columns have vanished again (`check:collection-identity`). |
Outputs
| Event | Payload type | Description |
|---|---|---|
| sortChange | SortState[] | Emitted with the new sort state when sorting changes. |
| selectionChange | Set<string | number> | Emitted with the set of selected row ids. |
| rowClick | T | Emitted with the clicked row. |
| cellClick | CellClickEvent<T> | Emitted with the clicked cell context. |
| editComplete | EditCompleteEvent<T> | Emitted when an inline cell edit completes (Pro). |
| retry | void | The host should re-run the failed load. |
| queryChange | SvDataQuery | Everything the user is asking to see, in the kit's shared shape. This is the output to wire in `serverSide` mode: one handler regardless of whether they paged, sorted, filtered or searched, and the same `SvDataQuery` every other component in the kit emits — so `@styloviz/data` and a hand-rolled fetch both take it without a translation layer. |
| serverPageChange | ServerPageEvent | Emitted in `serverSide` mode with the page, page size, sorts, column filters, search text and request id whenever the user changes any of them. |
| rowExpand | T | Emitted with the row when the user expands or collapses its detail panel. |
| bulkActionClick | { action: BulkAction; selectedIds: Set<string | number> } | Emitted with the bulk action and the selected row ids. |
| selectedIdsChange | Set<string | number> | Emits the new `selectedIds` whenever it changes — the output half of `[(selectedIds)]`. |
| expandedRowIdsChange | Set<string | number> | Emits the new `expandedRowIds` whenever it changes — the output half of `[(expandedRowIds)]`. |
| pageSizeChange | number | Emits the new `pageSize` whenever it changes — the output half of `[(pageSize)]`. |
| hiddenColumnKeysChange | Set<string> | Emits the new `hiddenColumnKeys` whenever it changes — the output half of `[(hiddenColumnKeys)]`. |
| revealedColumnKeysChange | ReadonlySet<string> | Emits the new `revealedColumnKeys` whenever it changes — the output half of `[(revealedColumnKeys)]`. |
<sv-data-table-pro>
Also in this family — SvDataTableProComponent from @styloviz-pro/data-table.
Async states
- loading
- empty
- error
- retry
- partial
Item identity & owned state
- Item key
rowKey— set this when your rows have noid, so selection survives a reorder.- Restorable state
- expandedRowIds
- hiddenColumnKeys
- revealedColumnKeys
- selectedIds
- selectionChange
Bind these to put a saved view back — the open sections, the hidden columns, the selected rows — and read their
Changeevents to store what the user does.
Saved state
- Stored kinds
- sv-data-table/layout
What you will find under your own storage key. You choose the key; the blob names its own
kindand schemaversion, so a component never reads state another one wrote.- Reading it back
- Restore through the component's own
restore…helper rather than casting the parsed JSON. A blob from an older release is migrated where it can be and refused with a reason where it cannot — so an upgrade never silently applies last month's shape to this month's fields.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| columns | TableColumn<T>[] | [] | Column definitions: key, label, alignment, and each column's sorting, filtering, rendering and editing options. |
| rows | readonly T[] | [] | The rows to render. `readonly` on purpose: the table never mutates this array, and anything that hands it one — a signal, a `computed`, `svDataController.items` — is handing over a value it still owns. A mutable `T[]` here rejected every one of those at the template boundary while promising a write the table does not perform. |
| rowKey | SvItemKey<T> | 'id' | What identifies a row: a property name, or a function for composite keys. Widened from `string` because a row identified by two fields — an order line keyed on `orderId` + `lineNo`, a matrix cell on `row` + `col` — had no way to say so, and the fallback silently keyed it on the whole row's JSON. See the identity contract on `resolveItemKey` in `@styloviz/core`. |
| selectable | boolean | false | Add a selection column of checkboxes (or radio buttons, per `selectMode`). |
| selectMode | SelectMode | 'multi' | 'multi' = checkboxes, 'single' = radio buttons. |
| selectedIds | Set<string | number> (two-way) | new Set() | Ids of the selected rows, as resolved by `rowKey` (two-way). |
| expandedRowIds | Set<string | number> (two-way) | new Set() | Which rows have their detail panel open (two-way). A `model` rather than internal state because it is the same kind of fact as `selectedIds`: a host restoring a table after a route change, or opening the row a deep link names, has to be able to write it — and one that persists layout has to be able to see it change. Held in a plain signal it was neither settable nor observable from outside. |
| actions | TableRowAction<T>[] | [] | Per-row actions, offered in a menu at the end of each row; an action's `hidden` callback can drop it for a given row. |
| bulkActions | BulkAction[] | [] | Actions shown in a bar above the table while any row is selected; each receives the selected ids and also emits `bulkActionClick`. |
| loading | boolean | false | Show the loading state (skeleton rows or an overlay spinner, per `loadingStyle`) and mark the table `aria-busy`. |
| loadingStyle | LoadingStyle | 'skeleton' | 'skeleton' rows or full-table 'overlay' spinner. |
| skeletonRows | number | 5 | Number of placeholder rows (and mobile cards) rendered while `loading` with the skeleton style. |
| paginated | boolean | true | Split rows into pages with a pagination bar. Has no effect when `virtualized` or `groupBy` is set. |
| pageSize | number (two-way) | 10 | Rows per page (two-way); also updated when the user picks a size in the pagination bar. |
| pageSizeOptions | number[] | [10, 25, 50] | Choices in the pagination bar's rows-per-page select; an empty array hides the select. |
| serverSide | boolean | false | Server-side mode: disables client filtering/sorting/paging. |
| totalCount | number | null | null | Total row count across all pages in `serverSide` mode, used for the range label and page count; falls back to `rows.length` when null. |
| virtualized | boolean | false | Windows row rendering to what's scrolled into view, using the same variable-height primitive (`createVariableVirtualizer`, `@styloviz/core`) `list-view` uses — a group header and a data row are different heights, so this table always needs the variable form, never the fixed one. Ignores `paginated`: virtualization and client pagination solve the same problem (a huge row set costing too much DOM), so turning this on reads as opting OUT of pagination rather than stacking both — the same relation `groupBy` has to pagination, and for the same reason. |
| virtualRowHeight | number | 44 | Approximate pixel height of one data row, used to compute the virtualized window. Deliberately approximate — same honesty as `list-view`'s `itemHeight`: measure your own rendered row height for pixel accuracy. A wrong guess still renders every row correctly, just with a slightly-off scrollbar length until the next scroll event corrects it. |
| virtualGroupRowHeight | number | 40 | Pixel height of a collapsible group header row, when `groupBy` is also set. |
| cardRowHeight | number | 96 | Pixel height of one `cardOnMobile` card, used to window the card list on the same `virtualized` switch as the table. A card is taller than a row and its height depends on how many columns are visible — a two-column grid of label/value pairs, so roughly `ceil(columns / 2) * 46 + 32`. Approximate for the same reason `virtualRowHeight` is: a wrong guess costs scrollbar accuracy, never correctness. |
| viewportHeight | number | 400 | Pixel height of the scrolling viewport when `virtualized` is on. A dedicated numeric input rather than reusing `maxHeight` — `maxHeight` accepts any CSS unit (`'50vh'`, `'none'`) that cannot be safely coerced to a pixel count, and the windowing math needs a real number to divide by. |
| groupBy | string | null | null | Column key to section rows by, with a collapsible header per distinct value. Single-level only — a deliberate scope line: nested/multi-level grouping (which parent collapses which children, aggregate-of-aggregates) is real complexity no showcase use case has asked for, and this shape is additive if a future version needs it. |
| defaultGroupsExpanded | boolean | true | Whether a group starts expanded or collapsed the first time it's seen. |
| variant | TableVariant | 'default' | Visual style: 'default', 'striped' (tinted alternate rows), 'bordered' (cell borders) or 'borderless' (no outer border or row dividers). |
| layout | TableLayout | 'default' | Layout mode. 'comparison' transposes rows into columns for a product-comparison grid. |
| highlightRowId | string | number | null | null | rowKey value of the product column to highlight as "recommended" in comparison layout. |
| transparent | boolean | false | Removes the table's own surface fill (root, toolbar, body and rows) so it inherits the background of its container — e.g. when nested inside a glass card or a coloured panel. Row hover/selection tints and the `striped` variant's odd-row tint are preserved. Also drops the card shadow. |
| density | TableDensity | 'comfortable' | Row padding and text size: 'compact', 'comfortable' or 'spacious'. A choice made in the density toggle overrides it. |
| compact | boolean | false | Deprecated shorthand for compact density. |
| maxHeight | string | null | null | Maximum height (any CSS length) of the table's scroll area before it scrolls vertically; ignored when `virtualized`, which uses `viewportHeight`. |
| showScrollHint | boolean | true | Show a fade gradient along the right edge hinting that the table scrolls horizontally. |
| rowClassFn | ((row: T) => string) | null | null | Callback returning extra Tailwind classes for a given row. |
| showToolbar | boolean | undefined | undefined | Show the toolbar strip. Left unset it DERIVES from whether the toolbar has anything to put in it — binding `tableTitle` or `[searchable]` used to render nothing at all unless you also remembered this flag, which is a silent no-op: the API accepted your input and ignored it. Set it explicitly to force the strip on (an empty bar as a spacer) or off (a title used only as the accessible name). Every existing call site passes it, so nothing changes for them. |
| tableTitle | string | '' | Heading shown in the toolbar; also the table's accessible name and the title of Excel and PDF exports. |
| searchable | boolean | false | Show a toolbar search box that filters rows by text in any column (sent as `search` in `serverSide` mode). |
| showColumnToggle | boolean | false | Show a toolbar menu for hiding and showing columns. |
| exportable | boolean | false | Show a toolbar export control offering the formats in `exportFormats`. |
| exportFormats | readonly TableExportFormat[] | ['csv'] | Which formats the export control offers, in menu order. One format renders a plain button; more than one renders a menu. Defaults to CSV alone so an existing `[exportable]="true"` keeps behaving exactly as before — Excel and PDF are opt-in, so a table only offers them where the host asks (PDF also fetches the `jspdf` / `html2canvas` engine as a lazy chunk on first use). |
| exportFileName | string | '' | Base filename for exports, without extension. |
| showDensityToggle | boolean | false | Show a toolbar control that lets the user switch row density. |
| columnFilters | boolean | false | Add a filter row beneath the header with a text or select filter per column (per each column's `filterType`). |
| multiSort | boolean | false | Let Shift+click on a header add a secondary sort instead of replacing the current one. |
| resizableColumns | boolean | false | Allow users to drag column edges to resize. |
| reorderableColumns | boolean | false | Allow users to drag headers (or Ctrl/Cmd+←/→) to reorder columns. |
| expandable | boolean | false | Add a toggle to each row that opens a detail panel beneath it. |
| expandRender | ((row: T) => string) | null | null | Returns the plain text shown in an expanded row's detail panel; when null, the panel lists every column's label and value. |
| showFooter | boolean | false | Show the summary footer row built from each column's `footerFn`; nothing renders unless at least one column defines one. |
| cardOnMobile | boolean | false | Below the `sm` breakpoint, render rows as stacked cards instead of the table. |
| emptyMessage | string | 'No data to display.' | Text shown when there are no rows to display. |
| customClass | string | '' | Extra CSS classes appended to the root element. |
| error | SvDataError | string | null | null | Load failure. Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`. Rendered in place of the rows, above the toolbar so the user can still change the filter that caused it. |
| hiddenColumnKeys | Set<string> (two-way) | new Set() | Which columns the user has hidden (two-way). A `model` rather than internal state so a saved table layout can be restored by binding it — the free tier had no other way in at all, and the Pro tier could only reach it through a whole-state snapshot. Declared here with the rest of the internal view state because that is where the column toggle lives; it is nonetheless part of the public API. |
| revealedColumnKeys | ReadonlySet<string> (two-way) | new Set() | Columns declared `hidden: true` that the user has since shown from the Columns menu. `hidden` is documented as "initially hidden — the user can show it from the column-toggle panel", but the table used to filter `!col.hidden` unconditionally: the column could never come back, and its checkbox (bound to `hiddenColumnKeys` alone) sat TICKED for a column that was not on screen. Two-way, like `hiddenColumnKeys`: together the two ARE the column-visibility view, and a host that persists one without the other restores a table whose revealed columns have vanished again (`check:collection-identity`). |
Outputs
| Event | Payload type | Description |
|---|---|---|
| sortChange | SortState[] | Emitted with the full list of active sorts (empty when cleared) whenever the user changes the sort. |
| selectionChange | Set<string | number> | Emitted with the new set of selected row ids whenever the user changes the selection. |
| rowClick | T | Emitted with the row when the user clicks a row or one of its cells, or activates it with Enter. |
| cellClick | CellClickEvent<T> | Emitted with the row, column and displayed value when the user clicks a cell; `rowClick` fires as well. |
| editComplete | EditCompleteEvent<T> | Emitted with the row, column, old and new value when an inline cell edit is committed with a valid, changed value, so the host can persist it. |
| retry | void | The host should re-run the failed load. |
| queryChange | SvDataQuery | Everything the user is asking to see, in the kit's shared shape. This is the output to wire in `serverSide` mode: one handler regardless of whether they paged, sorted, filtered or searched, and the same `SvDataQuery` every other component in the kit emits — so `@styloviz/data` and a hand-rolled fetch both take it without a translation layer. |
| serverPageChange | ServerPageEvent | Emitted in `serverSide` mode with the page, page size, sorts, column filters, search text and request id whenever the user changes any of them. |
| rowExpand | T | Emitted with the row when the user expands or collapses its detail panel. |
| bulkActionClick | { action: BulkAction; selectedIds: Set<string | number> } | Emitted with the bulk action and a snapshot of the selected row ids when the user runs a bulk action. |
| columnResize | { key: string; width: number } | A column finished resizing (pointer drag or ←/→ on a focused handle) — emits the column key and its new pixel width, so a host can persist the layout. |
| columnOrderChange | string[] | Columns were reordered by drag — emits the full column-key order, left to right. |
| exportFailed | { format: TableExportFormat; error: Error } | An export failed. PDF loads `jspdf` / `html2canvas` at click time, so a host that has not installed them learns about it here rather than through an unhandled rejection and a button that appears to do nothing. |
| groupToggle | { key: string; collapsed: boolean } | A group was expanded or collapsed by the user, with its new state. |
| layoutStateChange | TableLayoutState | Fires whenever anything `TableLayoutState` describes changes — column order/width/visibility, sort, column filters, page size, or which groups are collapsed — so a host knows there is something new to save. Store `serializeLayoutState()` rather than this payload: the payload is the live shape, while the serialized form carries a kind and a schema version, and `restoreLayoutState()` can then say why it ignored a blob instead of applying one whose shape no longer fits. Does not fire for the state the table mounted with, only for a real change after that, so a host writing straight through to storage does not immediately re-save what it just loaded via `applyLayoutState`. |
| selectedIdsChange | Set<string | number> | Emits the new `selectedIds` whenever it changes — the output half of `[(selectedIds)]`. |
| expandedRowIdsChange | Set<string | number> | Emits the new `expandedRowIds` whenever it changes — the output half of `[(expandedRowIds)]`. |
| pageSizeChange | number | Emits the new `pageSize` whenever it changes — the output half of `[(pageSize)]`. |
| hiddenColumnKeysChange | Set<string> | Emits the new `hiddenColumnKeys` whenever it changes — the output half of `[(hiddenColumnKeys)]`. |
| revealedColumnKeysChange | ReadonlySet<string> | Emits the new `revealedColumnKeys` whenever it changes — the output half of `[(revealedColumnKeys)]`. |
Wire it to your API
This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — it just saves you writing the debounce, the request guard and the retry policy by hand.
Template
Bound to the names this component actually exposes.
<sv-data-table [serverSide]="true" [rows]="data.items()" [loading]="data.loading()" [error]="data.error()" [totalCount]="data.total()" (queryChange)="data.setQuery($event)" (retry)="data.reload()" />
Host class
One controller handles debounce, request-guarding, cancellation and retry.
import { svDataController, createHttpDataSource } from '@styloviz/data'; readonly data = svDataController( createHttpDataSource<Row>({ url: '/api/rows' }), );
- `queryChange` carries an `SvDataQuery` — page, page size, sorts, filters, search and cursor in one object. One handler covers every way the user can change what they are asking for.
- `serverSide` switches **off** the built-in filtering, sorting and paging. Leave it false and the component works entirely from the array you pass — which is what you want for a few hundred rows.
- Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.
Full walkthrough: Connecting to a backend.
Customization & theming
- Set `[transparent]="true"` to remove the table's own surface fill (root, toolbar, body and rows) so it inherits the background of a glass card or coloured panel — hover/selection and `striped` tints are preserved and the card shadow is dropped; pair with `variant="borderless"`.
- Project rich cell content per column with the `svDtCell` directive: `<ng-template svDtCell="colKey" let-row let-value="value">…</ng-template>` (context exposes `$implicit` = row, `value`, `column`) — falls back to `col.render` text or a badge cell when no template is provided.
- Enable sorting, pagination, selection and bulk actions per table.
- Pro adds column filters, inline editing and expandable rows.
- Set `resizableColumns` to drag-resize columns (per-column `width` and `resizable` opt-out) and `reorderableColumns` to drag- or keyboard-reorder them; changes emit `columnResize` and `columnOrderChange`.
- Set `layout="comparison"` to transpose the table into a Compare-Products grid: the first column supplies each product's name (becoming the column headers) and the remaining columns become feature rows, with boolean values rendered as check / cross icons; `highlightRowId` spotlights the recommended product column.
- **Feature compatibility.** `virtualized` and `groupBy` each replace client pagination rather than combining with it: both render the whole sorted set into one scroller and the pager is hidden, so a `pageSize` alongside either is ignored.
- **Feature compatibility.** `serverSide` turns the client pipeline off entirely — filtering, sorting and paging all pass `rows` through untouched, because the server already did them. There is no double filtering, and equally no client-side fallback: whatever you do not implement server-side does not happen.
- **Feature compatibility.** `serverSide` composes with `groupBy`, but it groups only the page the server sent — a group header reads as a section of this page, not of the dataset.
- **Feature compatibility.** `footerFn`, export and selection all operate on rows in hand. Client-side that is the filtered set; in `serverSide` mode it is the current page, so a footer "Total" is a page total and a select-all selects the page.
- **Feature compatibility.** `layout="comparison"` is a separate rendering path: toolbar, filters, pagination, selection, virtualization, grouping and the footer row do not apply to it. Use it for a fixed handful of products, not as a table mode to toggle.
- **Feature compatibility.** `cardOnMobile` windows on its own `cardRowHeight` under `virtualized`, and deliberately does not group — a collapsible header spanning a card list has no column structure to summarise. Size it to roughly `ceil(visibleColumns / 2) * 46 + 32`px.
Best practices
- Do: Set a stable `rowKey` so selection and row tracking survive re-renders.
- Do: Switch to `serverSide` paging once datasets pass a few thousand rows.
- Don't: Don't render hundreds of unpaged rows — enable `paginated` or virtualize.
- Do: In `serverSide` mode, guard every response with `acceptsResponse(event.requestId)` before applying it. The table renders what the server sent verbatim, so if a slower request lands second the rows and the header disagree and nothing in your own code looks wrong.
- Do: Give an `editable` column a `validate` function. It runs on each keystroke and again on commit, so the message shows while the offending text is still on screen, and an invalid value never reaches `editComplete`.
- Don't: Don't read a `footerFn` total as a dataset total in `serverSide` mode — it sums the page the server sent. The same applies to export and to selection: both act on rows in hand. Ask the server for the aggregate instead.
Accessibility
- Keyboard at scale (X-10): the free table is paginated by default, so its page size is the ceiling. Under `virtualized` the rows are ONE tab stop across the whole dataset: ↑/↓, Home/End, PageUp/PageDown (one viewport, clamped) and typeahead over the first column move the cursor and scroll the window to it; Enter activates the row. Documented ceiling: 2,000 rows rendered at once with neither pagination nor `virtualized` — past it a dev-mode warning fires.
- Focus after a mutation (X-12): if the row holding focus leaves the rendered set — a filter it no longer matches, a host refresh, a delete — focus moves to the same control in the row now at its index, then the row before it, then the table's scroller, and a polite announcement says the row is no longer shown.
- Native table semantics with scoped headers and an accessible table name from `tableTitle`.
- Sortable headers are focusable and operable with Enter/Space, exposing `aria-sort` state.
- Resize handles are focusable separators (`role="separator"`) adjustable with ←/→; reorderable columns move from the keyboard with Ctrl/Cmd + ←/→ on a focused header.
- Selection, pagination and row actions are keyboard reachable; open menus close on Escape.
- Loading regions set `aria-busy`; row-action menus use `role="menu"`/`menuitem`.
- Under `virtualized`, the table declares `aria-rowcount` and stamps every rendered row with its true `aria-rowindex`, so a screen reader announces "row 140 of 241" rather than the size of the window. Header rows, the column-filter row, group headers, expanded detail rows and the footer row all take their place in that numbering; the spacer rows are `aria-hidden` and unnumbered. Both attributes are omitted whenever every row is already in the DOM, so the browser's own count stands and cannot be contradicted.
- A `validate` failure marks the editor `aria-invalid` and points `aria-describedby` at the message, which is also a polite live region — described-by covers arriving at the field, the live region covers an error appearing under a cursor already in it. Enter and blur refuse to commit while it stands; Escape always discards.
Feedback on Data Table
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.