Overview
Columns decide which fields are visible, in what order, at what width, and how each cell is rendered. You declare them as JSX children of DataTable — one DataTableColumn per visible field, with its header and cell renderer inside.
The JSX reads like a static description of the table, but DataTableColumn and its children don’t render in place. Each one registers a definition with the table, which then draws the virtualized header row and the visible body cells from those definitions. That indirection is what makes virtualization possible: a single cell renderer is reused across many row positions as the user scrolls, and headers compose cleanly with sticky pinning, resizing, slots, and visibility.
Basic columns
Section titled “Basic columns”import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader } from '@/components/ui/data-table'
import { localModel } from '@virtuoso.dev/data-table'
const rows = [
{ id: 'SKU-001', name: 'Standing Desk', category: 'Office', price: 699, stock: 14 },
{ id: 'SKU-002', name: 'USB-C Dock', category: 'Peripherals', price: 229, stock: 42 },
{ id: 'SKU-003', name: 'Mechanical Keyboard', category: 'Peripherals', price: 169, stock: 28 },
]
const currency = new Intl.NumberFormat('en-US', {
currency: 'USD',
style: 'currency',
})
const model = localModel({ data: rows })
export default function App() {
return (
<DataTable className="rounded-xl" model={model} style={{ height: 280 }}>
<DataTableColumn field="name">
<DataTableColumnHeader>Product</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="price">
<DataTableColumnHeader className="justify-end">Price</DataTableColumnHeader>
<DataTableCell className="text-right tabular-nums">{({ cellValue }) => currency.format(Number(cellValue))}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="stock">
<DataTableColumnHeader>Status</DataTableColumnHeader>
<DataTableCell>
{({ row }) => (
<span className={row.data.stock < 20 ? 'font-medium text-amber-600' : 'text-foreground'}>
{row.data.stock < 20 ? 'Low stock' : 'Healthy'}
</span>
)}
</DataTableCell>
</DataTableColumn>
</DataTable>
)
}What each part defines
Section titled “What each part defines”DataTableColumn— one column, keyed byfield.DataTableColumnHeader— header content and header class.DataTableCell— body-cell renderer and cell class.
Omit the header to fall back to the field name; omit the cell to fall back to the field value.
field is both the cell lookup key (row.data[field]) and the column’s persistent identity. Keep it stable across releases if you use any column features that save state — see State Persistence for the rules around renaming.
For layout, put base widths on DataTableColumnHeader and use DataTableColumn grow={...} only for columns that should absorb extra horizontal space. See Column Layout for the sizing model and guidance on choosing fixed vs growing columns from the data being displayed.
Where to go next
Section titled “Where to go next”Declarative shape:
- Defining Columns —
field, default rendering, declaration order, visibility, generated columns. - Formatting Cells and Headers — render params, extracted renderer functions, custom header content.
- Column Layout — base widths,
growratios, fixed metadata columns, and horizontal overflow. - Column Groups — wrap adjacent columns under a shared header.
- Sticky Columns — pin a column to the left or right edge during horizontal scrolling.
Interactive layout:
- Column Visibility — declarative defaults, runtime toggling, “Columns” picker.
- Column Resizing — drag handle in the header, presets, auto-fit.
- Column Reordering — drag-and-drop individual columns or whole groups.
Dynamic schemas:
- Generating Columns at Runtime — discovering and declaring columns when the list isn’t known up front.