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.
API mapping
Section titled “API mapping”| TableVirtuoso pattern | Data-table equivalent |
|---|---|
data array | localModel({ data: rows }) passed as model |
itemContent | DataTableColumn plus DataTableCell |
fixedHeaderContent | DataTableColumnHeader |
| grouped table rows | data.groups plus GroupHeaderCell |
| fixed columns | sticky="left" or sticky="right" |
| custom table pieces | shadcn wrapper classes or components overrides |
| programmatic scrolling | engineRef / engineId plus scrollToRow$ |
| range callbacks | onRenderedDataChange, viewportRange$, or scrollLocation$ |
Migration sequence
Section titled “Migration sequence”- Move row loading into Data Model.
- Define each visible column in Columns.
- Add opt-in features as needed: grouped rows, sticky columns, resizing, reordering, visibility.
- Replace
VirtuosoHandle-style commands with Controlling the Table. - Move table styling into the shadcn wrapper or customization slots.
Example shape
Section titled “Example shape”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.