Skip to content

Migrating From TableVirtuoso

@virtuoso.dev/data-table is not a drop-in replacement for TableVirtuoso. Virtualization still works the same; everything else — row loading, header structure, programmatic control — moves into explicit data model and column declarations.

TableVirtuoso patternData-table equivalent
data arraylocalModel({ data: rows }) passed as model
itemContentDataTableColumn plus DataTableCell
fixedHeaderContentDataTableColumnHeader
grouped table rowsdata.groups plus GroupHeaderCell
fixed columnssticky="left" or sticky="right"
custom table piecesshadcn wrapper classes or components overrides
programmatic scrollingengineRef / engineId plus scrollToRow$
range callbacksonRenderedDataChange, viewportRange$, or scrollLocation$
  1. Move row loading into Data Model.
  2. Define each visible column in Columns.
  3. Add opt-in features as needed: grouped rows, sticky columns, resizing, reordering, visibility.
  4. Replace VirtuosoHandle-style commands with Controlling the Table.
  5. Move table styling into the shadcn wrapper or customization slots.
import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader } from '@/components/ui/data-table'
import { localModel } from '@virtuoso.dev/data-table'

const rows = Array.from({ length: 40 }, (_, index) => ({
  id: `SKU-${String(index + 1).padStart(3, '0')}`,
  name: `Product ${index + 1}`,
  category: ['Office', 'Peripherals', 'Audio'][index % 3]!,
  status: ['In stock', 'Low stock', 'Backorder'][index % 3]!,
}))

const model = localModel({ data: rows })

export default function App() {
  return (
    <DataTable className="rounded-xl" computeRowKey={({ data }) => data.id} model={model} style={{ height: 340 }}>
      <DataTableColumn field="name">
        <DataTableColumnHeader>Name</DataTableColumnHeader>
        <DataTableCell>
          {({ row }) => (
            <div className="flex flex-col">
              <span className="font-medium">{row.data.name}</span>
              <span className="text-xs text-muted-foreground">{row.data.id}</span>
            </div>
          )}
        </DataTableCell>
      </DataTableColumn>
      <DataTableColumn field="category">
        <DataTableColumnHeader>Category</DataTableColumnHeader>
        <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
      </DataTableColumn>
      <DataTableColumn field="status" sticky="right">
        <DataTableColumnHeader>Status</DataTableColumnHeader>
        <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
      </DataTableColumn>
    </DataTable>
  )
}
<DataTable model={model} style={{ height: 420 }}>
  <DataTableColumn field="name">
    <DataTableColumnHeader>Name</DataTableColumnHeader>
    <DataTableCell>{({ row }) => row.data.name}</DataTableCell>
  </DataTableColumn>
  <DataTableColumn field="status" sticky="right">
    <DataTableColumnHeader>Status</DataTableColumnHeader>
    <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
  </DataTableColumn>
</DataTable>

The key shift: TableVirtuoso takes row renderers as children; data-table takes a data source and column declarations, with feature modules layered on top.