Skip to content

DataTable ​

A typed table that sorts, paginates, selects and exports, and turns into cards on a phone.

Open in demo
Open in demo

Click a header to sort, type a name to filter, tick rows, and page through. Narrow the window past 768px and the same table becomes cards.

The shape of it ​

vue
<template>
  <DataTable
    :columns="columns"
    :data="rows"
    :pagination="pagination"
    selectable
    row-key="id"
    @update:pagination="pagination = $event"
    @update:selection="selection = $event"
  />
</template>

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

Columns ​

ts
import type { ColumnDef } from '@shared/components/common/DataTable/types'

const columns: ColumnDef<Order>[] = [
  { key: 'id', label: 'Order', sortable: true, width: '90px' },
  { key: 'customer', label: 'Customer', sortable: true },
  { key: 'total', label: 'Total', sortable: true, align: 'right' },
  { key: 'placedAt', label: 'Placed', hideOn: 'sm' },
]
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.

Columns can come from a schema instead of being written by hand — see Schema.

The full API ​

Every prop, emit and slot, with its default, lives on the component page: Components → DataTable. The rest of this page is how the pieces fit together.

Sorting ​

Click a header to cycle ascending, descending, off. Sorting is local by default. Set serverSort and the cycle becomes ascending then descending, and the component only emits update:sort — your fetch does the work.

Selection ​

update:selection carries row keys, not row objects. That matches what a bulk action needs and stays correct on a server-paginated table, where a row selected on page one is no longer in data by the time the reader acts on page three.

ts
const bulkActions: BulkActionDef[] = [
  { label: 'Delete', icon: Trash2, variant: 'destructive', handler: keys => removeMany(keys) },
]

Row actions ​

ts
const actions: ActionDef<Order>[] = [
  { label: 'View', icon: Eye, handler: row => router.push(`/orders/${row.id}`) },
  { label: 'Refund', icon: Undo2, handler: refund, visibleIf: row => row.status === 'paid' },
  { label: 'Delete', icon: Trash2, variant: 'destructive', handler: remove },
]

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.

Filtering ​

The filter bar is its own component, so a page can place it where it belongs. It takes the same FieldDefinition[] the form engine uses.

vue
<template>
  <DataTableFilter
    v-model="filters"
    mode="collapsible"
    :fields="filterFields"
    @apply="refetch"
    @reset="refetch"
  />
</template>

mode is inline, collapsible, drawer or modal. Inline filters as the reader types, debounced by 300ms, and says so on the bar; the other three wait for Apply. It emits update:modelValue as values change, plus apply and reset, and counts the active filters for its badge, where active means a value that is not empty, null or an empty list.

Applying the filter to the rows is the page's job — the bar collects the values 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. The libraries behind Excel and PDF are imported on demand, so a page that never exports never downloads them.
  • Print opens the browser's print dialog on a print-only rendering of the table.
  • Import needs importFields. It offers a downloadable 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.

Column visibility ​

DataTableColumnToggle is deliberately not rendered by the table: a page decides where it belongs.

vue
<template>
  <DataTableColumnToggle v-model="visibleColumns" :columns="columns" />
  <DataTable :columns="columns" :data="rows" :visible-columns="visibleColumns" />
</template>

It remembers the reader's choice, keyed by route name unless you pass storageKey.

On a phone ​

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

Right to left ​

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. Nothing needs configuring: the component uses logical alignment throughout.

SchemaTable ​

When a table is nothing but the schema, skip the column definitions:

vue
<script setup lang="ts">
import SchemaTable from '@core/schema/SchemaTable.vue'
import { productFormSchema } from '../types/product.schemas'
</script>

<template>
  <SchemaTable :schema="productFormSchema" :data="products" />
</template>

Two props, schema and data. It renders every field without hideInTable, formats dates in the app's language, and re-renders the cells when the language changes. When you need sorting, selection or actions, use DataTable with schemaToColumns.

Where to go next ​

  • Schema — where the columns can come from.
  • Add a module — the CLI writes a list page built on this.