Skip to content

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.

ReferenceStatus
INV-1042Northwind Supplypaid€1,240.00
INV-1043Bluepeak Studiopending€480.50
INV-1044Harbour Logisticsoverdue€3,900.00
INV-1045Cedar & Copaid€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.

ReferenceStatusActions
INV-1042Northwind Supplypaid€1,240.00
INV-1043Bluepeak Studiopending€480.50
INV-1044Harbour Logisticsoverdue€3,900.00
INV-1045Cedar & Copaid€225.00

Nothing chosen yet.

Build the entries from the row, so each menu acts on the record beside it:

ts
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.

ReferenceStatus
INV-1042Northwind Supplypaid€1,240.00
INV-1043Bluepeak Studiopending€480.50
INV-1044Harbour Logisticsoverdue€3,900.00
INV-1045Cedar & Copaid€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.

ReferenceStatus

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:

vue
<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:

ts
const table = useDataTable({
  rows: () => rows.value,
  columns: () => columns,
})
// table.pageRows, table.toggleSort, table.headerSelection, …

Props

PropTypeDefaultDescription
rowsT[][]The rows to render; in server mode, the page the server returned
columnsTableColumn[][]Column definitions
totalnumberTotal rows on the server. Its presence switches to server mode
rowKeystring'id'Field identifying a row, used by selection and expansion
loadingbooleanSkeleton rows on the first load, a refresh bar after that
skeletonRowsnumber5How many skeleton rows the first load shows
selectablebooleanAdds the checkbox column
isRowSelectable(row) => booleanVeto selection per row
expandablebooleanAdds the expand chevron and the expanded slot
canExpandRow(row) => booleanVeto expansion per row
clickableRowsbooleanEmit rowClick, and show a pointer cursor
stripedbooleanAlternate row backgrounds
hoverablebooleantrueHighlight the row under the pointer
stickyHeaderbooleanKeep the header visible while the body scrolls
size'sm' | 'md' | 'lg''md'Row density
emptyTextstring'No results.'Line shown when there are no rows
actionsLabelstring'Actions'Names the blank header of the row-actions column
labelstringAccessible name for the table
captionstringVisible caption
unstyledbooleanDrop built-in classes
classstringMerged 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

ts
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

EventPayloadWhen
rowClickrowA row was clicked, with clickableRows set

Slots

SlotPropsWhen 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
emptyReplace the no-rows line
captionReplace the caption