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
totalnumber—Total rows on the server. Its presence switches to server mode
rowKeystring'id'Field identifying a row, used by selection and expansion
loadingboolean—Skeleton rows on the first load, a refresh bar after that
skeletonRowsnumber5How many skeleton rows the first load shows
selectableboolean—Adds the checkbox column
isRowSelectable(row) => boolean—Veto selection per row
expandableboolean—Adds the expand chevron and the expanded slot
canExpandRow(row) => boolean—Veto expansion per row
clickableRowsboolean—Emit rowClick, and show a pointer cursor
stripedboolean—Alternate row backgrounds
hoverablebooleantrueHighlight the row under the pointer
stickyHeaderboolean—Keep 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
labelstring—Accessible name for the table
captionstring—Visible caption
unstyledboolean—Drop built-in classes
classstring—Merged with the built-in classes
ui{ root?, table?, thead?, tbody?, tr?, th?, td?, empty?, caption? }—Per-element 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
empty—Replace the no-rows line
caption—Replace the caption