createDataTable
Composable data table built on v0 primitives. Composes sorting, filtering, pagination, selection, and expansion into a single pipeline.
Usage
Construct the table, then register columns via table.columns.onboard and rows via table.onboard. Each row becomes a ticket keyed by the id you supply — that id is what selection.toggle, expansion.toggle, and unregister accept. Columns are keyed by their own id field; that is what sort.toggle and the filter pipeline match against.
import { createDataTable } from '@vuetify/v0'
const table = createDataTable<User>()
table.columns.onboard([
{ id: 'name', title: 'Name', sortable: true, filterable: true },
{ id: 'email', title: 'Email', sortable: true, filterable: true },
{ id: 'role', title: 'Role', sortable: true },
])
table.onboard(users.map(value => ({ id: value.id, value })))
// Search
table.search('john')
console.log(table.query.value) // 'john'
// Sort — toggle cycles: none → asc → desc → none
table.sort.toggle('name')
// Paginate
table.pagination.next()
// Select rows
table.selection.toggle('user-1')
// Add / remove rows after setup
const ticket = table.register({ id: 'user-99', value: user })
ticket.unregister() // remove via returned ticket
table.unregister('user-1') // remove by id
table.clear() // wipe all rows
// Add / remove columns after setup
table.columns.register({ id: 'actions', title: '' })
table.columns.unregister('actions')
table.columns.clear()Reactivity
| Property / Method | Reactive | Notes |
|---|---|---|
items | Computed — final visible items (projected from registry tickets) | |
allItems | Computed — every registered row, unfiltered/unsorted | |
filteredItems | Computed — items after filtering | |
sortedItems | Computed — items after filter + sort | |
columns | RegistryContext — reactive column registry (columns.values() drives leaves and headers) | |
leaves | Computed — leaf columns (no children) used by the data pipeline | |
headers | Computed — 2D header grid with colspan/rowspan for rendering thead | |
query | ShallowRef — current search query (readonly) | |
sort.columns | Computed — current sort entries | |
pagination.page | ShallowRef — current page | |
pagination.items | Computed — visible page buttons | |
selection.selectedIds | shallowReactive(Set) — currently selected row IDs | |
selection.isAllSelected | Computed — all in scope selected | |
selection.isMixed | Computed — some but not all selected | |
expansion.expandedIds | shallowReactive(Set) — currently expanded row IDs | |
grouping.groups | Computed — grouped items | |
total | Computed — total row count | |
loading | Computed — adapter loading state | |
error | Computed — adapter error state | |
register(input) | — | Method — adds a single row ticket, mutates the row registry (downstream refs recompute) |
onboard(inputs) | — | Method — bulk register rows |
unregister(id) | — | Method — removes a row ticket by id |
clear() | — | Method — wipes every row ticket (useful before re-fetching server data) |
columns.register(input) | — | Method — adds a single column ticket (reactively updates leaves, headers, sort group, filter pipeline) |
columns.onboard(inputs) | — | Method — bulk register columns |
columns.unregister(id) | — | Method — removes a column by id; drops it from sort state |
columns.clear() | — | Method — wipes every column |
Adapters
Adapters control the data pipeline strategy. Pass one via the adapter option.
| Adapter | Pipeline | Use Case |
|---|---|---|
| ClientDataTableAdapter | filter → sort → paginate | Default. All processing client-side |
| ServerDataTableAdapter | pass-through | API-driven. Server handles filter/sort/paginate |
| VirtualDataTableAdapter | filter → sort → (no paginate) | Large lists rendered with createVirtual |
ClientDataTableAdapter (default)
All processing happens client-side. No constructor options — just use createDataTable without an adapter option.
Behavior:
Resets to page 1 on filter or sort changes
totalreflects the sorted item countNo
loadingorerrorstate (synchronous pipeline)
import { createDataTable } from '@vuetify/v0'
import { ClientDataTableAdapter } from '@vuetify/v0/data-table/adapters/client'
const table = createDataTable<User>({
adapter: new ClientDataTableAdapter(), // default — not required
})
table.columns.onboard(columns)
table.onboard(users.map(value => ({ id: value.id, value })))ServerDataTableAdapter
Pass-through adapter for API-driven tables. The server handles all filtering, sorting, and pagination — the client only renders what it receives.
Constructor options:
| Option | Type | Required | Description |
|---|---|---|---|
total | MaybeRefOrGetter<number> | Yes | Total item count on the server (drives pagination) |
loading | MaybeRefOrGetter<boolean> | No | Loading state (e.g., from useFetch) |
error | MaybeRefOrGetter<Error | null> | No | Error state from API calls |
Behavior:
Resets to page 1 on filter or sort changes
allItems,filteredItems,sortedItems, anditemsall point to the same source (no client-side processing)Exposes
loadinganderrorviatable.loadingandtable.error
Server-backed tables don’t hold a long-lived items ref — instead, the fetch handler calls table.clear() and table.onboard(...) whenever a new page of results comes back. The registry becomes the single source of truth for what the table renders, and the adapter’s total / loading / error refs drive pagination and UI state.
import { createDataTable } from '@vuetify/v0'
import { ServerDataTableAdapter } from '@vuetify/v0/data-table/adapters/server'
const total = shallowRef(0)
const loading = shallowRef(false)
const error = shallowRef<Error | null>(null)
const table = createDataTable<User>({
adapter: new ServerDataTableAdapter({ total, loading, error }),
})
table.columns.onboard(columns)
async function load () {
loading.value = true
const result = await fetchPage(/* query, sorts, page */)
total.value = result.total
table.clear()
table.onboard(result.items.map(value => ({ id: value.id, value })))
loading.value = false
}
// Watch query/sort/page to trigger API calls
watch(
[table.query, table.sort.columns, table.pagination.page],
() => load(),
{ immediate: true },
)VirtualDataTableAdapter
Client-side filtering and sorting without pagination slicing. All sorted items are returned for use with createVirtual at the rendering layer.
Behavior:
No constructor options — instantiate with
new VirtualDataTableAdapter()Resets on filter or sort changes
No
loadingorerrorstate
import { createDataTable, createVirtual } from '@vuetify/v0'
import { VirtualDataTableAdapter } from '@vuetify/v0/data-table/adapters/virtual'
const table = createDataTable<User>({
adapter: new VirtualDataTableAdapter(),
})
table.columns.onboard(columns)
table.onboard(rows.map(value => ({ id: value.id, value })))
// Wrap table.items with createVirtual for rendering
const virtual = createVirtual(table.items, { itemHeight: 40 }) Rows and columns are registered through the registry surface, not passed as factory options. Call onboard for bulk registration or register for one entry at a time — for rows the ticket id IS the row identifier, so selection, expansion, and grouping all key off it; for columns the id field is what sort.toggle, the filter pipeline, and the adapter all key off.
// Columns (live under table.columns)
table.columns.onboard([
{ id: 'name', title: 'Name', sortable: true },
{ id: 'email', title: 'Email', filterable: true },
])
// Rows (top-level — bulk)
table.onboard(rows.map(value => ({ id: value.id, value })))
// Rows — one at a time
table.register({ id, value })Features
Sorting
Toggle sort cycles through directions. Configure with mandate and firstSortOrder.
const table = createDataTable<User>({
mandate: true, // asc → desc → asc (never clears)
firstSortOrder: 'desc', // First click sorts descending
sortMultiple: true, // Enable multi-column sort
})
table.columns.onboard([
{ id: 'name', sortable: true },
{ id: 'age', sortable: true, sort: (a, b) => Number(a) - Number(b) },
])
table.onboard(items.map(value => ({ id: value.id, value })))
table.sort.toggle('name')
table.sort.direction('name') // 'asc' | 'desc' | 'none'
table.sort.priority('name') // 0-based index, or -1
table.sort.columns.value // [{ key: 'name', direction: 'asc' }]
table.sort.order // ['name'] — multi-sort priority array
table.sort.reset() // Clear all sort stateFiltering
Search filters across all filterable columns. Use per-column filter for custom logic.
const table = createDataTable<User>()
table.columns.onboard([
{ id: 'name', filterable: true },
{ id: 'status', filterable: true, filter: (value, query) => {
return String(value).toLowerCase() === query.toLowerCase()
} },
])
table.onboard(items.map(value => ({ id: value.id, value })))
table.search('active')Selection
Control row selection with the selectStrategy option.
| Strategy | Behavior |
|---|---|
'single' | Only one row selected at a time |
'page' | selectAll/toggleAll operate on visible page (default) |
'all' | selectAll/toggleAll operate on all filtered items |
const table = createDataTable<User>({
selectStrategy: 'page',
itemSelectable: 'canSelect', // Disable selection for rows where canSelect is falsy
})
table.columns.onboard(columns)
table.onboard(items.map(value => ({ id: value.id, value })))
table.selection.toggle('row-1')
table.selection.isSelected('row-1') // true
table.selection.isSelectable('row-1') // true (based on itemSelectable)
table.selection.toggleAll()
table.selection.isAllSelected.value // true
table.selection.isMixed.value // falseExpansion
Expand rows to reveal detail content.
const table = createDataTable<User>({
expandMultiple: false, // Only one row expanded at a time
})
table.columns.onboard(columns)
table.onboard(items.map(value => ({ id: value.id, value })))
table.expansion.toggle('row-1')
table.expansion.isExpanded('row-1') // true
table.expansion.expandAll()
table.expansion.collapseAll()Dynamic columns
Columns are a registry, so they can be added, removed, or replaced at any point — not just at construction. leaves, headers, the sort group, and the filter pipeline all react to column changes. Use this for user-toggled visibility, plugin-injected columns, or columns that arrive asynchronously with their schema.
const table = createDataTable<User>()
// Initial columns
table.columns.onboard([
{ id: 'name', title: 'Name', sortable: true },
{ id: 'email', title: 'Email' },
])
// Later: add a column at runtime
table.columns.register({ id: 'actions', title: '' })
// Remove a column — drops it from headers, leaves, and sort state
table.columns.unregister('email')
// Replace the column set entirely
table.columns.clear()
table.columns.onboard(nextColumns)Grouping
Group rows by a column value.
const table = createDataTable<Employee>({
groupBy: 'department',
openAll: true, // Auto-open all groups
})
table.columns.onboard(columns)
table.onboard(items.map(value => ({ id: value.id, value })))
table.grouping.groups.value // [{ key: 'Engineering', value: 'Engineering', items: [...] }]
table.grouping.toggle('Engineering')
table.grouping.isOpen('Engineering')
table.grouping.openAll()
table.grouping.closeAll()Examples
Benchmarks
Every operation is profiled across multiple dataset sizes to measure real-world throughput. Each benchmark is assigned a performance tier—good, fast, blazing, or slow—and groups are scored by averaging their individual results so you can spot bottlenecks at a glance. This transparency helps you make informed decisions about which patterns scale for your use case. Learn more in the benchmarks guide.