Skip to content

Group Rows by Region and Category

This example precomputes a flattened grouped result, then lets the table virtualize group headers and product rows together. Regions are top-level group rows, categories are second-level group rows, and products remain ordinary data rows.

  • localModel({ data, groups }) receives the flattened row stream and group markers.
  • groups: [{ index, level }] identifies which rows are group headers and how deeply they nest.
  • GroupHeaderCell renders the marked group rows instead of rendering column cells for them.
import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader, GroupHeaderCell } from '@/components/ui/data-table'
import { localModel } from '@virtuoso.dev/data-table'

import { groupedProducts } from './data'
import type { GroupLabel, Product } from './data'

const model = localModel(groupedProducts)

function rowKey(row: GroupLabel | Product) {
  return 'id' in row ? row.id : row.groupKey
}

function productName(row: GroupLabel | Product) {
  return 'name' in row ? row.name : ''
}

export default function App() {
  return (
    <DataTable className="rounded-xl" computeRowKey={({ data }) => rowKey(data)} model={model} style={{ height: 420 }}>
      <GroupHeaderCell>
        {({ level, row }) => (
          <div className={level === 0 ? 'bg-muted px-3 py-2 font-semibold' : 'bg-muted/50 px-6 py-2 font-medium'}>
            {(row.data as { label: string }).label}
          </div>
        )}
      </GroupHeaderCell>

      <DataTableColumn field="name">
        <DataTableColumnHeader>Product</DataTableColumnHeader>
        <DataTableCell className="font-medium">{({ row }) => productName(row.data)}</DataTableCell>
      </DataTableColumn>
      <DataTableColumn field="status">
        <DataTableColumnHeader>Status</DataTableColumnHeader>
        <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
      </DataTableColumn>
      <DataTableColumn field="region">
        <DataTableColumnHeader>Region</DataTableColumnHeader>
        <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
      </DataTableColumn>
    </DataTable>
  )
}

Group labels are virtualized rows, so deep nesting still scrolls cheaply. The practical limit is readability: two levels is usually the ceiling before labels need to be very short. If the grouping changes at runtime, keep stable keys on both product rows and group rows so React can preserve row identity as sections move.