Skip to content

Defining Columns

DataTableColumn declares a column. Its field prop does double duty: it’s the row-data lookup key and the public column identifier used by persistence, reordering, resizing, and visibility.

<DataTableColumn field="name">
  <DataTableColumnHeader>Name</DataTableColumnHeader>
  <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>

For a row { name: 'Standing Desk' }, the cell renderer receives 'Standing Desk' as cellValue. Omitting DataTableCell falls back to the field value; omitting DataTableColumnHeader falls back to the field name.

Add an explicit header or cell when you need formatting; leave both off when the field name and stringified value are enough.

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', stock: 14 },
  { id: 'SKU-002', name: 'USB-C Dock', category: 'Peripherals', stock: 42 },
  { id: 'SKU-003', name: 'Mechanical Keyboard', category: 'Peripherals', stock: 28 },
]

const model = localModel({ data: rows })

export default function App() {
  return (
    <DataTable className="rounded-xl" model={model} style={{ height: 260 }}>
      <DataTableColumn field="name">
        <DataTableColumnHeader>Product</DataTableColumnHeader>
        <DataTableCell className="font-medium">{({ cellValue }) => String(cellValue)}</DataTableCell>
      </DataTableColumn>
      <DataTableColumn field="category" />
      <DataTableColumn field="stock">
        <DataTableColumnHeader className="justify-end">Stock</DataTableColumnHeader>
        <DataTableCell className="text-right tabular-nums">{({ cellValue }) => String(cellValue)}</DataTableCell>
      </DataTableColumn>
    </DataTable>
  )
}

Columns render in declaration order. Reordering and persistence change the runtime order; the declaration order stays as the baseline used after a reset.

<DataTable model={model}>
  <DataTableColumn field="name" />
  <DataTableColumn field="category" />
  <DataTableColumn field="price" />
</DataTable>

visible={false} hides a column by default. Runtime visibility lives in the Column Visibility page.

<DataTableColumn field="internalSku" visible={false} />

sticky="left" or sticky="right" pins a column for row identity, IDs, or actions that should stay visible during horizontal scrolling — see Sticky Columns.

<DataTableColumn field="name" sticky="left" />
<DataTableColumn field="actions" sticky="right" />

For dynamic field lists, map them to DataTableColumn declarations. Keep the field order deterministic — the field values become the column keys.

const fields = ['name', 'category', 'stock']

;<DataTable model={model}>
  {fields.map((field) => (
    <DataTableColumn field={field} key={field}>
      <DataTableColumnHeader>{field}</DataTableColumnHeader>
      <DataTableCell>{({ cellValue }) => String(cellValue ?? '')}</DataTableCell>
    </DataTableColumn>
  ))}
</DataTable>

When the field list itself is discovered from the first data result, see Runtime Columns.