Skip to main content
Vuetify0 is now in beta!
Vuetify0 Logo
Theme
Mode
Palettes
Accessibility
Vuetify One
Sign in to Vuetify One

Access premium tools across the Vuetify ecosystem — Bin, Play, Studio, and more.

Not a subscriber? See what's included

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.

ts
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 layout

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.

ModuleBuilt onPurpose
table (spread)createDataTableSearch, sort, filter, paginate, total — all v-modeled through
layoutgrid.columns + createGroupReads 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
editinginternal factoryClick-to-edit lifecycle, per-column validation, dirty tracking
rowscreateSortablePost-sort row reordering, layered in the grid’s items projection over the sorted rows before pagination — not inside the adapter
spanscomputed mapRow 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.

AdapterPipelineUse Case
ClientDataTableAdapter (default)filter → sort → paginateAll processing client-side
ServerGridAdapterpass-throughAPI-driven. Server handles everything
VirtualDataTableAdapterfilter → sort → (no paginate)Large lists with createVirtual
ts
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 ordering

ServerGridAdapter

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.

ts
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.

ts
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

PropertyReactiveNotes
itemsFinal visible items (filter + sort + row order + paginate)
allItemsRaw unprocessed items (projected from registered tickets)
filteredItemsItems after filtering
sortedItemsItems after filter + sort
layout.columnsResolved columns with size/offset (render set — visible only)
layout.allEvery column incl. hidden, each with a visible flag
layout.pinnedPin region breakdown
editing.activeCurrently edited cell
editing.errorValidation error string
editing.dirtyUncommitted edits map
rows.orderCurrent row ordering
spansRow span map
headers2D header grid
sort.columnsCurrent sort entries
pagination.pageCurrent page
totalTotal 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.

ts
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.

ts
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` flag

Cell Editing

Click-to-edit with validation. Does not mutate source data — commit fires a callback.

ts
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.

ts
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 sorts

Row Spanning

Merge cells vertically using a spanning function.

ts
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.

ts
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' }]]

API Reference

The following API details are for the createDataGrid composable.
Was this page helpful?

Ctrl+/