Skip to content

DataTable ​

A typed table with sorting, selection, row and bulk actions, inline editing, export and a card layout on phones.

Open in demo
Open in demo

Usage ​

Two props are required: columns and data. Everything else turns a piece on.

Pagination and sorting are controlled. The component never slices data: it shows exactly the rows you hand it and tells you when the reader asks for another page or another order. That is the same contract for a client-side list and a server-paginated one, so moving between them changes your fetch and nothing else.

rowKey names the property that identifies a row. It defaults to id, resolved at runtime.

For how the pieces fit together, and for SchemaTable, see Systems → DataTable.

Examples ​

Custom cells ​

render returns a string or a VNode and owns the whole cell, with no wrapper element around it. width, align and hideOn are per column.

Selection and bulk actions ​

selectable adds the checkbox column and the select-all header. update:selection carries row keys, not rows, which is what a bulk action needs and what stays correct on a server-paginated table where a row picked on page one is no longer in data by page three. BulkActionDef.handler receives the same keys.

Row actions ​

Two or fewer visible actions render as inline buttons; three or more collapse into an overflow menu. visibleIf is evaluated per row, so a table with four actions can still show inline buttons on a row where only one applies. actionsMenu forces the menu either way.

Expandable rows ​

expandable prepends an arrow column and renders the #expanded-row slot beneath the row it belongs to.

Inline editing ​

A column with editable swaps to an input on double-click. Enter and blur commit through cell-edit, Escape reverts. It is table mode only, and a column with a render is never editable: the cell's author owns the cell.

The table does not write to your data. It hands you the row, the key and the value, and you decide.

Loading and empty ​

loading renders a skeleton of pagination.perPage rows, five when there is no pagination, or five cards in card mode. With no rows and no loading, the table shows the shared empty state, which the #empty slot replaces.

features.skeletonLoaders off removes the skeleton rows and cards. See Global config.

Column visibility ​

DataTableColumnToggle is deliberately not rendered by the table, so a page can put it where it belongs. It remembers the reader's choice in localStorage, keyed by the route name unless you pass storageKey.

visibleColumns is a read-only input on the table. You own the state.

Filter bar ​

DataTableFilter is its own component and takes the same FieldDefinition[] the form engine uses. mode is inline, collapsible, drawer or modal. Inline filters as the reader types, debounced by 300ms; the other three wait for Apply. Applying the values to the rows is the page's job: the bar collects them and says when.

The toolbar ​

Export and print are on by default, import is opt-in. Export writes CSV, Excel or PDF of the current data and columns, each cell as the table shows it (see useExport), and the Excel and PDF libraries are imported on demand, so a page that never exports never downloads them. Import needs importFields: it offers a template built from those fields, parses the uploaded file, coerces each column to its declared type, and emits import with the rows. Nothing is saved until you save it.

Cards ​

Below 768px each row becomes a card, which is how a wide table stays readable without a horizontal scroll. mobileCards: false opts out. displayMode="card" forces cards at every width.

API ​

Source: src/shared/components/common/DataTable/

Props ​

PropTypeDefaultDescription
columnsColumnDef<T>[]requiredThe columns, in render order.
dataT[]requiredThe rows to show, already paged.
loadingbooleanfalseRenders a skeleton of pagination.perPage rows, or five when there is no pagination.
paginationPaginationConfig—{ page, perPage, total }. Omit it and no pager renders.
selectablebooleanfalseAdds the checkbox column and the select-all header.
displayMode'table' | 'card''table'Force one layout at every width.
serverSortbooleanfalseStop sorting locally and only emit update:sort.
rowKeykeyof T & string'id'The property that identifies a row.
actionsActionDef<T>[]—Row actions.
bulkActionsBulkActionDef[]—Actions over the current selection.
actionsMenubooleanfalseForce row actions into the overflow menu.
visibleColumnsstring[]—Keys to show. Unset means all. Read-only: you own the state.
expandablebooleanfalseAdds the expand arrow and renders #expanded-row.
mobileCardsbooleantrueCards below 768px.
toolbarbooleantrueThe top bar with import, export and print.
exportablebooleantrueThe export menu.
printablebooleantrueThe print button.
importablebooleanfalseThe import dialog. Needs importFields.
importFieldsFieldDefinition[][]Drives the downloadable template and the coercion on import.
filenamestring'export'Filename stem for exports and the template.
cardbooleantrueThe card the toolbar, table and pager sit in.

Emits ​

EventPayloadDescription
update:paginationPaginationConfigThe reader changed page or page size.
update:sortSortConfig<T>{ key, direction }.
update:selectionT[keyof T][]Row keys, not rows.
row-clickTFires on a row press.
cell-editCellEditPayload<T>{ row, key, value }, after Enter or blur on an editable cell.
importRecord<string, unknown>[]Parsed and coerced rows from the import dialog.

Slots ​

SlotPropsDescription
toolbar-start—Content at the start of the toolbar, before the built-in buttons.
toolbar—Content after toolbar-start, inside the toolbar.
empty—Replaces the empty state.
expanded-rowrow: TThe panel under an expanded row. Needs expandable.

ColumnDef<T> ​

KeyTypeDescription
keykeyof T & stringThe property to read.
labelstringHeader text. A translation key or a literal.
sortablebooleanAdds the sort control to this header.
filterablebooleanMarks the column as filterable for a filter bar.
widthstringAny CSS width, such as '120px' or '20%'.
align'left' | 'center' | 'right'Cell alignment.
render(value, row) => string | VNodeCustom cell output, with no wrapper element.
hideOn'sm' | 'md' | 'lg'Hide this column below that width.
editablebooleanDouble-click swaps the cell for an input. Table mode only, and a render always wins.

ActionDef<T> and BulkActionDef ​

KeyTypeDescription
labelstringA translation key or a literal.
iconComponentOptional.
handler(row: T) => void / (keys: unknown[]) => voidA row action takes the row; a bulk action takes the selected keys.
variant'default' | 'destructive'Destructive draws in the destructive color.
visibleIf(row: T) => booleanRow actions only. Evaluated per row.

The section components ​

Each piece is usable on its own. The table renders the toolbar, bulk-action bar, row actions and pager itself; the filter and column toggle are placed by the page.

ComponentPropsEmits
DataTableToolbardata, columns, filename, exportable, printable, importable, importFieldsimport
DataTableFiltermode, fields, modelValueupdate:modelValue, apply, reset
DataTableColumnTogglecolumns, modelValue, storageKeyupdate:modelValue
DataTableExportdata, columns, filename—
DataTableImportDialogopen, fields, filenameupdate:open, confirm
DataTableBulkActionsselected, actionsclear
DataTableActionsactions, row, menu—
DataTablePaginationpaginationupdate:pagination

DataTableToolbar also has a start slot, which is what the table's toolbar-start fills.

Page sizes offered by the pager are 10, 25, 50 and 100, plus the current perPage when it is not one of them, so a table set to 5 shows its own size rather than an empty box.

Accessibility ​

  • A sortable header cell shows the current direction with an icon, so the state does not rest on color, and the cycle is ascending, descending, off. It is a clickable header cell rather than a button, so sorting is a pointer gesture.
  • The select-all checkbox is named "Select all rows on this page" and reports the indeterminate state when only some rows on the page are selected. Each row checkbox is named "Select row".
  • The expand arrow is a button carrying aria-expanded, named "Expand row" or "Collapse row", and its icon rotates, so the state is not carried by position alone.
  • The actions column's header carries a visually hidden "Actions", so the column is not an unnamed blank.
  • An inline edit commits on Enter or blur and reverts on Escape, and its two buttons are named "Apply this change" and "Discard this change". They hold the input's focus on mouse-down, or blur would commit before Discard ran.
  • In a right-to-left layout the columns reverse with the page, the actions column stays at the inline end, and the expand arrow flips, because the component uses logical alignment throughout.