createDataGrid
A headless data grid with column layout, cell editing, row ordering, and row spanning.
Usage
Construct a grid, onboard columns through grid.columns with size percentages, then register rows to get column layout, search, sort, and pagination. Columns are onboarded — not passed as a factory option — so the surface matches createDataTable and columns can be added or removed at any time.
import { createDataGrid } from '@vuetify/v0'
const grid = createDataGrid()
// Onboard columns through the inherited column registry
grid.columns.onboard([
{ id: 'name', title: 'Project', sortable: true, filterable: true, size: 22 },
{ id: 'status', title: 'Status', sortable: true, size: 12 },
{ id: 'assignee', title: 'Assignee', sortable: true, size: 16 },
{ id: 'progress', title: 'Progress', sortable: true, size: 14 },
{ id: 'budget', title: 'Budget', sortable: true, size: 10 },
])
// Register rows through the inherited registry surface
grid.onboard(projects.map(value => ({ id: value.id, value })))
// Inherited from createDataTable
grid.search('alice')
grid.sort.toggle('name')
grid.pagination.next()
// Grid-specific: column layout
grid.layout.columns.value // ResolvedColumn[] with size, offset, pinned
grid.layout.pin('name', 'left')
grid.layout.resize('name', 5) // grow by 5%, neighbor shrinks
grid.layout.reorder(0, 2) // move column 0 to position 2
grid.layout.hide('budget') // exclude from the render set
grid.layout.reset() // restore initial layoutArchitecture
createDataGrid composes createDataTable — which owns the data pipeline (filter, sort, paginate) — with four grid modules: column layout, cell editing, row ordering (createSortable), and row spanning. Columns and rows are both onboarded through registries (grid.columns.onboard(...), grid.onboard(...)) rather than passed as options, the same shape as createDataTable. Per-column config (size, pinned, editable, validate, span) rides on each column ticket, so layout, editing, and spanning read it straight off grid.columns and pick up columns onboarded at any time.
| Module | Built on | Purpose |
|---|---|---|
table (spread) | createDataTable | Search, sort, filter, paginate, total — all v-modeled through |
layout | grid.columns + createGroup | Reads column order and config from the column registry; layers tri-region pinning, percentage sizing, delta-based resize, and visibility (show / hide / toggle / all) on top |
editing | internal factory | Click-to-edit lifecycle, per-column validation, dirty tracking |
rows | createSortable | Post-sort row reordering, layered in the grid’s items projection over the sorted rows before pagination — not inside the adapter |
spans | computed map | Row span resolution and hidden-cell tracking |
Adapters
The grid uses the standard data table adapters — row ordering is layered above the pipeline, not inside it, so any DataTableAdapter works without modification.
| Adapter | Pipeline | Use Case |
|---|---|---|
ClientDataTableAdapter (default) | filter → sort → paginate | All processing client-side |
| ServerGridAdapter | pass-through | API-driven. Server handles everything |
VirtualDataTableAdapter | filter → sort → (no paginate) | Large lists with createVirtual |
import { createDataGrid } from '@vuetify/v0'
const grid = createDataGrid()
// ClientDataTableAdapter is the default — no adapter option required
grid.columns.onboard(columns)
grid.onboard(employees.map(value => ({ id: value.id, value })))
// Row ordering — id-based
grid.rows.move(employees[0].id, 3) // move that row to position 3
grid.rows.reset() // clear custom orderingServerGridAdapter
Pass-through adapter for API-driven grids — the server owns sort, filter, and pagination. Re-exports the data table’s ServerDataTableAdapter.
Onboard only the rows the server returns for the current page, and set total to the full server-side count. The grid renders that page on every page — onboard page 2’s rows, advance pagination.page, and the grid surfaces them. Because the page window can sit past the locally-held rows, the grid orders the onboarded page in place rather than re-slicing it into emptiness.
import { createDataGrid, ServerGridAdapter } from '@vuetify/v0'
const grid = createDataGrid({
adapter: new ServerGridAdapter({ total: totalCount, loading: isLoading }),
})
grid.columns.onboard(columns)
// On every page change, clear and onboard the rows the server returned for
// that page; keep `total` at the full server count. `grid.clear()` wipes
// the row registry — onboarded columns are untouched.
grid.clear()
grid.onboard(rows.map(value => ({ id: value.id, value })))Virtual scrolling
For large datasets, use the standard VirtualDataTableAdapter. Row ordering still applies; pagination slicing is skipped.
import { createDataGrid, VirtualDataTableAdapter } from '@vuetify/v0'
const grid = createDataGrid({
adapter: new VirtualDataTableAdapter(),
})
grid.columns.onboard(columns)
grid.onboard(largeDataset.map(value => ({ id: value.id, value })))Reactivity
| Property | Reactive | Notes |
|---|---|---|
items | Final visible items (filter + sort + row order + paginate) | |
allItems | Raw unprocessed items (projected from registered tickets) | |
filteredItems | Items after filtering | |
sortedItems | Items after filter + sort | |
layout.columns | Resolved columns with size/offset (render set — visible only) | |
layout.all | Every column incl. hidden, each with a visible flag | |
layout.pinned | Pin region breakdown | |
editing.active | Currently edited cell | |
editing.error | Validation error string | |
editing.dirty | Uncommitted edits map | |
rows.order | Current row ordering | |
spans | Row span map | |
headers | 2D header grid | |
sort.columns | Current sort entries | |
pagination.page | Current page | |
total | Total row count |
Examples
Recipes
Column Layout
Columns are onboarded through grid.columns, sized as percentages (0–100), and can be pinned, resized, reordered, and hidden.
const grid = createDataGrid()
grid.columns.onboard([
{ id: 'name', size: 30, pinned: 'left', minSize: 15, maxSize: 50 },
{ id: 'email', size: 40 },
{ id: 'status', size: 30, pinned: 'right' },
])
grid.onboard(rows.map(value => ({ id: value.id, value })))
// Pin regions
grid.layout.pinned.value // { left: [...], scrollable: [...], right: [...] }
// Resize — delta-based, neighbor absorbs inverse
grid.layout.resize('name', 5) // name grows 5%, email shrinks 5%
// Reorder by display index
grid.layout.reorder(0, 2)
// Replace all sizes at once
grid.layout.distribute([40, 35, 25])
// Restore initial state
grid.layout.reset()Column Visibility
Hide and show columns without redistributing the remaining widths — headless, so the consumer rebalances via distribute() or CSS. all surfaces every column (including hidden ones) each carrying a visible flag, which is exactly the shape a column chooser needs.
const grid = createDataGrid()
grid.columns.onboard(columns)
grid.onboard(rows.map(value => ({ id: value.id, value })))
grid.layout.hide('email') // exclude from the render set
grid.layout.show('email') // restore it
grid.layout.toggle('email') // flip current visibility
grid.layout.columns.value // render set — visible columns only
grid.layout.all.value // every column, each with a `visible` flagCell Editing
Click-to-edit with validation. Does not mutate source data — commit fires a callback.
const grid = createDataGrid({
editing: {
onEdit: (row, column, value, item) => {
console.log(`Updated ${column} on row ${row} to ${value}`)
},
},
})
grid.columns.register({
id: 'email',
editable: true,
validate: (value, item) => {
if (typeof value !== 'string' || !value.includes('@')) return 'Invalid email'
return true
},
})
grid.onboard(rows.map(value => ({ id: value.id, value })))
grid.editing.edit(1, 'email') // Activate cell
grid.editing.commit('new@email') // Validate and save
grid.editing.cancel() // Discard
grid.editing.active.value // { row: 1, column: 'email' } | null
grid.editing.error.value // 'Invalid email' | null
grid.editing.dirty // Map of uncommitted edits (ShallowReactive, no .value)Row Ordering
Post-sort row ordering for drag-and-drop reordering. Backed by createSortable, keyed by row id — index-based addressing was dropped because it drifts under reactive churn.
const grid = createDataGrid()
grid.columns.onboard(columns)
grid.onboard(rows.map(value => ({ id: value.id, value })))
grid.rows.move(rowId, 3) // Move the row with this id to position 3
grid.rows.order.value // Current id sequence
grid.rows.reset() // Clear custom ordering
// Ordering resets on sort change by default
// Set preserveRowOrder: true to keep ordering across sortsRow Spanning
Merge cells vertically using a spanning function.
const grid = createDataGrid({
rowSpanning: (item, column) => {
if (column === 'department') return 3 // span 3 rows
return 1
},
})
grid.columns.onboard(columns)
grid.onboard(rows.map(value => ({ id: value.id, value })))
// Span map: item ID → column id → { rowSpan, hidden }
grid.spans.value.get(1)?.get('department')
// { rowSpan: 3, hidden: false } — render with rowspan="3"
grid.spans.value.get(2)?.get('department')
// { rowSpan: 1, hidden: true } — skip rendering (covered by row above)Nested Columns
Column definitions support nesting for grouped headers. Layout and data pipeline use leaf columns only.
const grid = createDataGrid()
grid.columns.onboard([
{ id: 'name', title: 'Name', size: 30 },
{
id: 'contact',
title: 'Contact',
children: [
{ id: 'email', title: 'Email', size: 40 },
{ id: 'phone', title: 'Phone', size: 30 },
],
},
])
grid.onboard(rows.map(value => ({ id: value.id, value })))
// headers: 2D array with colspan/rowspan for <thead> rendering
grid.headers.value
// [[{ id: 'name', rowspan: 2 }, { id: 'contact', colspan: 2 }],
// [{ id: 'email' }, { id: 'phone' }]]