📊 DataTable Component
The DataTable is a powerful, generic table component built with Svelte 5 Runes. It provides a rich set of features similar to Excel or advanced data grids.
⚡ Features
- Sorting: Click column headers to sort ASC/DESC.
- Filtering: Excel-style column filters (Text, Number, Date, Enum, Multi-enum, Size, Currency).
- Pagination: Client-side pagination in a floating balloon, with configurable page sizes — see Pagination.
- Selection: Row selection with checkboxes, "Select All" on the current page, and a "show selected only" filter in the checkbox header.
- Column Management: Resize columns in the table; show/hide and drag-to-reorder through
ColumnVisibilityToggle, which the caller places in its own toolbar and hands the table instance (tableRef, plusadditionalTableRefsto drive several tables at once); it works through the public methods. - Persistence: Page size, column order, widths and visibility overrides are saved to
localStorageunder per-user keys (lf_{userId}_dataTable_{storageKey}_{suffix}). Sorting and filters are not persisted; filters can round-trip through the URL (initialFilters,onFiltersChange). - Actions: Per-row actions behind a ⋮ button in the actions column, and bulk actions on the
selected rows. Actions with
requireConfirmgo through a ConfirmModal. - Context Menu: Right-click on a row opens the same menu as ⋮, built from
rowActions. - Sticky Columns: The header, the selection column and pinned columns stick; the actions column sticks by default — see Sticky columns.
💻 Usage
<script lang="ts">
import { DataTable } from '$lib/components/table';
import type { ColumnDef, RowAction } from '$lib/components/table/types';
import { Pencil, Trash2 } from 'lucide-svelte';
// Define your data type
type User = { id: string; name: string; role: string; };
// Define columns
const columns: ColumnDef<User>[] = [
{
id: 'name',
header: 'Name',
// Simple text cell
cell: (row) => row.name,
sortable: true,
filterable: true,
type: 'text'
},
{
id: 'role',
header: 'Role',
// Badge cell
cell: (row) => ({
type: 'badge',
text: row.role,
variant: row.role === 'admin' ? 'success' : 'default'
}),
type: 'enum',
enumOptions: [
{ value: 'admin', label: 'Admin' },
{ value: 'user', label: 'User' }
]
}
];
// Define row actions
const rowActions: RowAction<User>[] = [
{
id: 'edit',
icon: Pencil,
label: 'Edit',
onClick: (row) => console.log('Edit', row)
},
{
id: 'delete',
icon: Trash2,
label: 'Delete',
variant: 'danger',
requireConfirm: true,
onClick: (row) => console.log('Delete', row)
}
];
</script>
<DataTable
data={users}
columns={columns}
rowActions={rowActions}
getRowId={(row) => row.id}
storageKey="users-table"
/>
📋 Props
The source of truth is the Props interface of lib/components/table/DataTable.svelte.
Data and identity
| Prop | Type | Default | Description |
|---|---|---|---|
data |
T[] |
Required | Rows to show. |
columns |
ColumnDef<T>[] |
Required | Column definitions. |
getRowId |
(row: T) => string |
Required | Unique id of a row. |
storageKey |
string |
Required | Key of the persisted preferences. |
fullData |
T[] |
— | Full dataset for the filter boundaries (min/max, currencies) when data is a pre-paginated slice. |
isLoading |
boolean |
false |
Shows a loading row (dt-loading). |
emptyMessage |
string |
common.noData |
Text of the empty table. |
tableLayout |
'fixed' \| 'auto' |
'fixed' |
auto lets columns grow to fill the space. |
Selection
| Prop | Type | Default | Description |
|---|---|---|---|
enableSelection |
boolean |
true |
false is the same as selectionMode="none". |
selectionMode |
'multi' \| 'single' \| 'none' |
'multi' |
Checkboxes, single-row click selection, or none. |
selectionColumnWidth |
string |
'48px' |
Width of the checkbox column. |
initialSelectedIds |
string[] |
— | Rows selected at mount (e.g. returning to a wizard step). |
selectedRowId |
string \| null |
null |
Controlled selection in single mode. |
onSelectionChange |
(ids: string[]) => void |
— | Called when the selection changes. |
isRowSelectable |
(row: T) => boolean |
— | false hides the checkbox (⊘ instead) and excludes the row from bulk selection. |
disabledRowTooltip |
(row: T) => string \| null |
— | Tooltip of the ⊘ icon. |
enableTouchSelection |
boolean |
false |
A 500 ms long-press toggles the row's selection. Without it, a long-press runs onRowDoubleClick. |
onShowSelectedOnlyChange |
(active: boolean) => void |
— | The "show selected only" toggle changed. |
Rows and actions
| Prop | Type | Default | Description |
|---|---|---|---|
onRowClick |
(row: T) => void |
— | Row click. |
onRowDoubleClick |
(row: T) => void |
— | Row double-click. |
getRowHref |
(row: T) => string \| undefined |
— | Middle-click opens the URL in a new tab. |
getRowClass, getRowStyle |
(row: T) => string |
— | Extra classes / inline style per row. |
getRowDisplayName |
(row: T) => string |
— | Name of a row in the confirmation lists (falls back to the id). |
enableActions |
boolean |
true |
Shows the actions column when rowActions is given. |
rowActions |
RowAction<T>[] \| (row: T) => RowAction<T>[] |
[] |
Actions of each row (⋮ menu and context menu). |
bulkActions |
BulkAction<T>[] |
[] |
Actions on the selected rows. |
actionsColumnWidth |
string |
'64px' |
Width of the actions column. |
actionsLabel |
string |
table.actions |
Header and ⋮ label of the actions column. |
stickyActions |
boolean |
true |
Whether the actions column sticks to the right edge — see Sticky columns. |
enableContextMenu |
boolean |
true |
Right-click menu on rows. |
Columns, sorting, filters
| Prop | Type | Default | Description |
|---|---|---|---|
enableSorting |
boolean |
true |
Column sorting. |
onSortChange |
(state: SortState \| null) => void |
— | Sort changed or cleared. |
onRowOrderChange |
(rowIds: string[]) => void |
— | Row ids in display order (after filters and sort, across all pages); called after mount and whenever that order changes. |
enableColumnFilters |
boolean |
true |
Column filters. |
initialFilters |
Record<string, FilterValue> |
— | Filters applied at mount (from the URL). |
onFiltersChange |
(filters) => void |
— | Filters changed (for URL sync). |
enableColumnResize |
boolean |
true |
Column resizing. |
onColumnResize |
(columnId: string, width: number) => void |
— | A column was resized (to mirror widths across tables). |
stickyHeader |
boolean |
true |
Whether the header sticks to the top of the scroll viewport. |
footerCells |
FooterCells<T> |
— | Aggregate footer, computed on the selected rows, or on the filtered rows when nothing is selected. |
Pagination
| Prop | Type | Default | Description |
|---|---|---|---|
enablePagination |
boolean |
true |
Client-side pagination. |
defaultPageSize |
number |
10 |
Page size until the user picks one (the stored choice wins). |
pageSizeOptions |
number[] |
[10, 25, 50, 100, 0] |
Choices of the page-size menu; 0 means "all" (shown as ∞). |
alwaysShowPagination |
boolean |
false |
Show the bar whenever there is at least one row. |
enableColumnVisibility is accepted but not read: column visibility is driven from outside, through
ColumnVisibilityToggle and the public methods.
📌 Sticky columns
- The header sticks to the top of the vertical scroll viewport (
stickyHeader, default on). - The selection column and the columns with
pinned: 'left' | 'right'stick to their edge while the table scrolls horizontally. - The actions column sticks to the right edge by default. With
stickyActions={false}it scrolls with the table, and its header stays at the end of the table, aligned with its cells. The transactions table, the bulk editor and the import wizard tables turn it off. - At 768 px and below, the actions column never sticks.
📄 Pagination
The pagination bar (DataTablePagination.svelte) floats in a balloon that sticks to the bottom of
the scroll area. It appears when pagination is on and the filtered rows outnumber the smallest
non-zero entry of pageSizeOptions — or, with alwaysShowPagination, as soon as there is one row.
Changing the page size returns to the first page and is saved under the table's storageKey.
The page-size menu is positioned with fixed coordinates computed from its trigger
(getFixedDropdownPosition, lib/utils/layout/dropdownPosition.ts): it opens below the trigger,
or above it when there is no room, stays inside the viewport, scrolls internally when taller than
the screen, and follows the trigger on resize and on any scroll. So in a short table inside a
scrollable modal the menu is not clipped by the modal's overflow and every option stays reachable.
Short tables can offer smaller steps: the bulk editor and the import wizard pass
pageSizeOptions={[5, 10, 25, 50, 0]}-style lists.
🧭 Public methods
Bind the component (bind:this={tableRef}) to call them.
| Method | Description |
|---|---|
navigateToRowId(rowId) |
Moves to the page that holds the row (with the current filters and sort), highlights it with a short pulse and scrolls it into view (smooth, centred). Unknown ids are ignored. The highlight clears on the next row click or key press in the table. |
getSortedRowIds() |
Ids of all matching rows in display order, before pagination — check it first to tell a hidden row from a missing one. |
getPageRowIds() |
Ids of the rows on the current page. |
clearSelection(), toggleRowSelectionById(id) |
Selection control. |
clearFilters() |
Removes every column filter (and reports it through onFiltersChange). |
getColumnsForVisibility(), toggleColumnVisibilityById(id), setColumnOrder(ids), resetColumnLayout(), setColumnWidth(id, width) |
Column layout, for ColumnVisibilityToggle and for syncing widths across tables. |
navigateToRowId is how a banner or a list sends the user to a row: the bulk transaction editor
jumps to the row of a validation issue or of a todo (getSortedRowIds() first, and a warning toast
when the row is filtered out); the lots panel, DataEditor ("Add row", chart point click) and
TransactionsTable use it as well. The highlighted row carries data-highlighted="true".
🔧 Column Definition (ColumnDef<T>)
The main fields; lib/components/table/types.ts has the full interface.
| Field | Type | Description |
|---|---|---|
id |
string |
Unique identifier for the column. |
header |
string \| () => string |
Header text (a function for i18n). headerHtml replaces it with markup; displayName names the column in the visibility menu. |
cell |
(row: T) => CellContent |
Cell content, see below. |
type |
'text' \| 'number' \| 'date' \| 'enum' \| 'multi-enum' \| 'size' \| 'currency-stack' \| 'custom' |
Drives the filter UI and the sort. |
getValue |
(row: T) => unknown |
Raw value for sorting and filtering, when it differs from the rendered cell. |
sortable, filterable, resizable |
boolean |
Default true. |
width, minWidth, maxWidth |
number |
Pixels. |
enumOptions |
EnumOption[] |
{value, label, iconUrl?, dotColor?, searchText?} for enum columns. |
getMultiValue |
(row: T) => string[] |
Values of a multi-enum row (e.g. tags). |
getCurrencyValue, currencyOptions |
— | currency-stack columns: {code, amount} of a row, and the currencies on offer. |
hiddenByDefault |
boolean |
Hidden until the user shows it. |
pinned |
'left' \| 'right' |
Sticky column. |
align |
'left' \| 'center' \| 'right' |
Cell and header alignment. |
headerTooltip, headerTooltipUrl |
string \| () => string |
Info icon on the header, optionally linking a documentation page. |
urlKey |
string |
URL parameter of the column's filter (default: the id). |
integerOnly |
boolean |
Integer-only number filter. |
🎨 Cell Content Types
The cell function can return a simple string/number or a structured object:
- Text:
stringornumber - Icon + Text:
{ type: 'icon-text', icon: Component, text: string, iconClass?: string } - Image:
{ type: 'image', src: string, alt: string, text?: string, fallbackIcon?: Component, size?: number, circle?: boolean }(thumbnail with fallback icon, default 32 px) - Badge:
{ type: 'badge', text: string, variant: 'success'|'warning'|'error'|'info'|'default', customStyle?: string } - Date:
{ type: 'date', value: Date | string, format?: 'date'|'datetime'|'time'|'relative' } - Size:
{ type: 'size', bytes: number }(Auto-formats to KB/MB/GB, i18n-aware) - Link:
{ type: 'link', text: string, href: string, external?: boolean } - HTML:
{ type: 'html', html: string, tooltip?: {...}, onClick?: () => void, testId?: string } - Editable:
editable-number,editable-text,editable-select,editable-checkbox— inline inputs that report through theironchange - Custom:
{ type: 'custom', component: Component, props: object }
html is rendered as-is
html cells, headerHtml and html footer cells go through {@html} without sanitising.
Escape every user- or provider-supplied value with escapeHtml
($lib/utils/core/escapeHtml.ts) when you build the string. The gate
frontend/src/htmlInterpolation.gate.test.ts flags HTML templates that interpolate such a
value unescaped.
📦 State Management (Internal)
The component uses Svelte 5 Runes for internal state:
$statefor sorting, pagination, filters, selection and the column layout.$derivedfor calculating filtered/sorted/paginated data efficiently.- Preferences are read from
localStorageon mount and written back when they change. - The pure decision logic — cell value extraction, filter predicates, typed comparison, min/max and
enum options — lives in
dataTableLogic.ts, unit-tested indataTableLogic.test.ts;DataTable.test.tscovers the wiring and the public methods.