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 layoutAdapters
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 })))Architecture
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 |
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
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 |
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 |
| 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' }]]FAQ
createDataGrid composes createDataTable and layers on column layout (pin, resize, reorder, hide), cell editing, row ordering, and row spanning. Use the table when you only need the data pipeline; reach for the grid when you need spreadsheet-style column manipulation or editable cells.
Row ordering resets on sort change by default. Pass preserveRowOrder: true to keep the custom rows.order across sort changes.
Call grid.layout.resize(id, delta) — it grows that column by a percentage and shrinks its neighbor inversely so the columns always total 100%. grid.layout.reset() restores the initial sizes, order, and pins.
grid.editing.commit(value) runs the column’s validate function first — the commit only lands when the validator returns true, and grid.editing.error holds the rejection string until the value passes or the user cancels.
No — grid.layout.hide(id) just drops the column from the render set; rebalancing is left to you via grid.layout.distribute([...]) or CSS. grid.layout.all lists every column with a visible flag, which is exactly the shape a column chooser needs.
Yes. Onboard columns with a children array and read grid.headers — a 2D grid carrying colspan / rowspan for <thead>. The layout and data pipeline still operate on the leaf columns only.
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.