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
Column Pinning & Resizing
A financial data grid with 10 columns that requires horizontal scrolling. Ticker is pinned left, sector pinned right — the center columns scroll independently with drag-to-resize handles.
File breakdown:
| File | Role |
|---|---|
data.ts | 12 stocks across Tech, Healthcare, Finance, Energy, and Consumer sectors |
columns.ts | 10 columns with ticker pinned left, sector pinned right |
usePinnedGrid.ts | Layout logic — pin/unpin, drag-resize, pixel sticky offsets, and the up/down/volume stats |
PinnedToolbar.vue | Market-overview header with the ticker filter and reset |
PinnedFooter.vue | Row count plus the left / scrollable / right pin-region summary |
PinnedGrid.vue | The spreadsheet table — sticky pinned columns, resize handles, formatted numbers |
Key patterns:
layout.pinnedsplits columns intoleft,scrollable, andrightregions with independent offsetslayout.resize(id, delta)adjusts a column and its neighbor to maintain total widthlayout.pin(id, position)moves columns between regions dynamicallylayout.reset()restores initial sizes, order, and pins
Ticker | Company | Price | Change | Volume | Mkt Cap | P/E | EPS | Div % | Sector |
|---|---|---|---|---|---|---|---|---|---|
| AAPL | Apple Inc. | $198.11 | +1.24% | 54.3M | $3.1T | 32.4 | $6.11 | 0.55% | Tech |
| MSFT | Microsoft Corp. | $452.37 | -0.38% | 22.1M | $3.4T | 37.1 | $12.20 | 0.72% | Tech |
| GOOGL | Alphabet Inc. | $176.89 | +2.15% | 28.7M | $2.2T | 25.6 | $6.91 | 0.00% | Tech |
| JNJ | Johnson & Johnson | $155.42 | -0.72% | 8.4M | $374.0B | 15.8 | $9.84 | 2.96% | Healthcare |
| UNH | UnitedHealth Group | $527.63 | +0.89% | 3.2M | $486.0B | 21.3 | $24.77 | 1.42% | Healthcare |
| PFE | Pfizer Inc. | $27.14 | -1.53% | 41.6M | $153.0B | 45.2 | $0.60 | 5.88% | Healthcare |
| JPM | JPMorgan Chase | $205.88 | +0.67% | 9.8M | $592.0B | 12.1 | $17.02 | 2.34% | Finance |
| GS | Goldman Sachs | $478.21 | -0.14% | 2.1M | $158.0B | 16.7 | $28.63 | 2.10% | Finance |
| XOM | Exxon Mobil Corp. | $104.56 | +1.87% | 15.9M | $438.0B | 13.4 | $7.80 | 3.45% | Energy |
| CVX | Chevron Corp. | $152.73 | -0.91% | 7.3M | $284.0B | 14.9 | $10.25 | 4.12% | Energy |
Cell Editing
An inventory management grid where editing is the primary workflow. Product name, price, and quantity are editable; invalid values show inline errors and block commit. Every committed edit pushes a { from, to } entry onto a createTimeline, which powers the Undo / Redo buttons and the history log.
File breakdown:
| File | Role |
|---|---|
data.ts | 8 products across electronics, accessories, and peripherals |
columns.ts | All columns editable + sortable, with validate functions |
useEditableGrid.ts | Editing state — grid + createTimeline, edit / commit / cancel and undo / redo handlers |
EditToolbar.vue | Inventory stats, edited-count chip, and Undo / Redo / Clear controls |
EditHistory.vue | The timeline-backed edit history log |
EditableGrid.vue | The editable table — click-to-edit cells with Enter / Escape / Ctrl+Z keyboard handling |
Key patterns:
editing.edit(row, column)activates a cell for editing — the cell paintsbg-primary/10so the edit target is unmistakableediting.commit(value)validates first — onlytruefrom the validator allows the edit throughediting.errorpersists until the value passes validation or the user cancelsonEditcallback fires after a successful commit; the example pushes{ row, column, from, to }to acreateTimeline({ size: 50 })timeline.undo()/timeline.redo()walk the history; the example applies the recoveredfrom(undo) orto(redo) to the row in place
Product | SKU | Price | Qty | Category |
|---|---|---|---|---|
| Wireless Mouse | WM-1001 | $29.99 | 150 | Peripherals |
| Mechanical Keyboard | MK-2010 | $89.99 | 75 | Peripherals |
| USB-C Hub | UH-3022 | $49.99 | 200 | Accessories |
| 27" Monitor | MN-4005 | $349.99 | 30 | Electronics |
| Webcam HD | WC-5011 | $59.99 | 120 | Peripherals |
| Laptop Stand | LS-6003 | $39.99 | 90 | Accessories |
| Bluetooth Speaker | BS-7019 | $79.99 | 60 | Electronics |
| Noise-Cancelling Headphones | NC-8042 | $199.99 | 45 | Electronics |
Row Spanning
A portfolio holdings grid with two levels of row spanning — account spans every holding under an account, and assetClass spans every holding within an account-and-class pair. Spanned cells double as aggregation rows by showing the account or asset-class subtotal alongside the label.
File breakdown:
| File | Role |
|---|---|
data.ts | 11 holdings across 3 accounts (Wealth, Retirement, Trust) and 4 asset classes (Equities, Bonds, Real Estate, Cash) |
columns.ts | 6 columns: account, asset class, ticker, holding, value, change (pinned right) |
useSpanningGrid.ts | Spanning logic — the rowSpanning callback and account / asset-class aggregation helpers |
SpanningGrid.vue | The table — multi-level row spans, subtotals in spanned cells, the pinned Today column |
Key patterns:
One
rowSpanning(item, column)callback resolves both span levels by checking whether the next consecutive row shares the sameaccount(and, forassetClass, the same account-and-class pair)spans.value.get(rowId).get(columnId)returns{ rowSpan, hidden }— render<td>only when!hidden, and set:rowspanfromrowSpanSpanned cells display aggregate information (account total, asset-class subtotal) so the spanned row carries domain meaning beyond visual grouping
Cells with
hidden: trueare skipped in rendering — the cell above covers themSpans are clamped to remaining visible rows and never cross page boundaries
| Account | Asset Class | Ticker | Holding | Value | Today |
|---|---|---|---|---|---|
Wealth Account$1,200,000 | Equities$720,000 | AAPL | Apple Inc. | $180,000 | 2.4% |
| MSFT | Microsoft Corp. | $245,000 | 1.8% | ||
| NVDA | NVIDIA Corp. | $295,000 | 5.2% | ||
Bonds$300,000 | US-10Y | Treasury Note 10Y | $150,000 | -0.3% | |
| GS-AAA | Goldman Corp. AAA | $150,000 | 0.1% | ||
Real Estate$180,000 | VNQ | Vanguard REIT | $180,000 | 0.9% | |
Retirement$580,000 | Equities$400,000 | VOO | S&P 500 ETF | $230,000 | 1.4% |
| VTI | Total Market ETF | $170,000 | 1.6% | ||
Cash$180,000 | MMF | Money Market Fund | $180,000 | 0.0% | |
Trust$450,000 | Equities$250,000 | GOOGL | Alphabet Class A | $250,000 | 3.1% |
rowSpanning callback that walks the source order. 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' }]]