Skip to content

Defining Columns

DataTableColumn declares a column. For data-backed columns, pass field; it reads row.data[field], uses that value as cellValue, and uses the field name as the stable column identity for 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 on a field-backed column falls back to the field name. In app UIs, prefer explicit headers so you control capitalization, wording, and accessibility.

Display-only columns do not need matching row fields. Give them an explicit id instead. The id becomes the stable column identity, and cellValue is undefined unless you provide an accessor.

<DataTableColumn id="actions" sticky="right">
  <DataTableColumnHeader>Actions</DataTableColumnHeader>
  <DataTableCell>{({ row }) => <ActionsMenu row={row.data} />}</DataTableCell>
</DataTableColumn>

id is not a visible label. For action, selection, expansion, status-icon, and other display-only columns, always provide a DataTableColumnHeader. Use visible text such as Actions by default; use sr-only text only when the product intentionally wants a headerless icon column.

Computed columns also use an explicit id, plus an accessor when you want the table to provide a derived cellValue.

<DataTableColumn id="fullName" accessor={(row) => `${row.firstName} ${row.lastName}`}>
  <DataTableColumnHeader>Full name</DataTableColumnHeader>
  <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>

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 id="actions" sticky="right" />

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

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.