Data display
ITable
Sorting, selection, expansion and per-cell slots. Columns are plain objects, and the rows are yours to supply — in client or server mode.
| Reference | Status | ||
|---|---|---|---|
| INV-1042 | Northwind Supply | €1,240.00 | |
| INV-1043 | Bluepeak Studio | €480.50 | |
| INV-1044 | Harbour Logistics | €3,900.00 | |
| INV-1045 | Cedar & Co | €225.00 |
A #cell-<key> slot replaces the content of that column's cells and receives { row, value }. Everything else falls back to the raw value.
Row actions
Fill the row-actions slot and the table adds a trailing column for it, as narrow as its content and pinned to the end. The header is blank, named for screen readers by actionsLabel.
| Reference | Status | Actions | ||
|---|---|---|---|---|
| INV-1042 | Northwind Supply | €1,240.00 | ||
| INV-1043 | Bluepeak Studio | €480.50 | ||
| INV-1044 | Harbour Logistics | €3,900.00 | ||
| INV-1045 | Cedar & Co | €225.00 |
Nothing chosen yet.
Build the entries from the row, so each menu acts on the record beside it:
function actionsFor(invoice) {
return [
{ label: 'View', icon: ViewIcon, onSelect: () => open(invoice) },
{ label: 'Duplicate', icon: CopyIcon, onSelect: () => duplicate(invoice) },
'-',
{ label: 'Delete', icon: TrashIcon, danger: true, onSelect: () => remove(invoice) },
]
}A click inside the column never reaches the row, so clickableRows and a menu can coexist. Pair a destructive entry with useConfirm().
Selection
Bind v-model:selection to get a checkbox column, with the header checkbox handling the indeterminate state for you. It holds row keys, not row objects.
| Reference | Status | |||
|---|---|---|---|---|
| INV-1042 | Northwind Supply | €1,240.00 | ||
| INV-1043 | Bluepeak Studio | €480.50 | ||
| INV-1044 | Harbour Logistics | €3,900.00 | ||
| INV-1045 | Cedar & Co | €225.00 |
Selected: none
Client or server
The presence of total decides the mode, and nothing else:
- Absent — client mode. The table sorts and slices the rows it was given.
- Present — server mode. The table renders exactly the rows you pass and emits state changes for you to act on.
In server mode the table tells you when the page or sort changes and renders what you hand back. Fetching, caching and cancellation stay in your data layer.
Server mode
Bind v-model:page and v-model:sort, watch them, and pass back the page the server returned along with total. Sort a column or change the page below — each one issues a request, and loading holds the row skeleton while it is in flight.
| Reference | Status | ||
|---|---|---|---|
sort is { key, order } or null, where key is the column's sortKey when it has one and its key otherwise — so a column can display customer.name and sort by customer_name.
onCleanup runs when the inputs change again, which makes it the place to abort the previous request — otherwise a slow first response can land after a fast second one and leave the wrong page on screen.
State ownership
Every model is optional. Bind one and you own that piece of state; leave it unbound and the table keeps it internally — so the page, sort and selection can live in a store, in the URL, or nowhere at all:
<ITable
v-model:page="page"
v-model:sort="sort"
v-model:selection="selected"
:columns="columns"
:rows="rows"
:total="total"
/>Without the markup
useDataTable() is the sorting, paging and selection logic on its own, for when you want the behaviour but not the markup:
const table = useDataTable({
rows: () => rows.value,
columns: () => columns,
})
// table.pageRows, table.toggleSort, table.headerSelection, …Props
| Prop | Type | Default | Description |
|---|---|---|---|
rows | T[] | [] | The rows to render; in server mode, the page the server returned |
columns | TableColumn[] | [] | Column definitions |
total | number | — | Total rows on the server. Its presence switches to server mode |
rowKey | string | 'id' | Field identifying a row, used by selection and expansion |
loading | boolean | — | Skeleton rows on the first load, a refresh bar after that |
skeletonRows | number | 5 | How many skeleton rows the first load shows |
selectable | boolean | — | Adds the checkbox column |
isRowSelectable | (row) => boolean | — | Veto selection per row |
expandable | boolean | — | Adds the expand chevron and the expanded slot |
canExpandRow | (row) => boolean | — | Veto expansion per row |
clickableRows | boolean | — | Emit rowClick, and show a pointer cursor |
striped | boolean | — | Alternate row backgrounds |
hoverable | boolean | true | Highlight the row under the pointer |
stickyHeader | boolean | — | Keep the header visible while the body scrolls |
size | 'sm' | 'md' | 'lg' | 'md' | Row density |
emptyText | string | 'No results.' | Line shown when there are no rows |
actionsLabel | string | 'Actions' | Names the blank header of the row-actions column |
label | string | — | Accessible name for the table |
caption | string | — | Visible caption |
unstyled | boolean | — | Drop built-in classes |
class | string | — | Merged with the built-in classes |
ui | { root?, table?, thead?, tbody?, tr?, th?, td?, empty?, caption? } | — | Per-slot class overrides |
Models: v-model:sort, v-model:page, v-model:perPage, v-model:selection and v-model:expanded.
Column shape
interface TableColumn<T = any> {
/** Accessor path into the row, dot-notation for nested values. Also the slot suffix. */
key: string
label?: string
sortable?: boolean
/** Sort by a different field than the one displayed. */
sortKey?: string
align?: 'start' | 'center' | 'end'
/** Tabular figures; implies end alignment unless `align` says otherwise. */
numeric?: boolean
/** Inline width, e.g. '12rem', or '1px' to shrink to content. */
width?: string
/** Extra classes on every cell in the column; a function receives the row. */
class?: string | ((row: T) => string)
/** Hide the column outright, or per row (the cell renders empty). */
hidden?: boolean | ((row: T) => boolean)
}
interface TableSort {
key: string
order: 'asc' | 'desc'
}Events
| Event | Payload | When |
|---|---|---|
rowClick | row | A row was clicked, with clickableRows set |
Slots
| Slot | Props | When to use it |
|---|---|---|
cell-<key> | { row, column, value } | Replace the contents of that column's cells |
row-actions | { row } | A per-row menu, in a trailing column of its own |
header-<key> | { column } | Replace that column's header |
expanded | { row } | The panel under an expanded row |
empty | — | Replace the no-rows line |
caption | — | Replace the caption |