DataGrid

A powerful data grid component with sorting, filtering, and custom cells. (Work in Progress)
Import
JSX
import DataGrid from '@cronocode/react-box/components/dataGrid';

Keyboard and roles

This is the APG grid pattern, over a virtualized body — the case most grids skip. The scrolling element is a role="grid" of rowgroups, rows and gridcells, and it tells assistive technology the size of the whole grid rather than the size of the window it renders: aria-rowcount counts every row, aria-rowindex numbers each one where it really is, and a jump to a row nobody has scrolled to brings it into view first. You write none of that.
Focus lives on the cells
One cell is in the tab order at a time, so Tab enters the grid in a single press instead of walking through thousands of them. The arrow keys move that cell; a sortable header sorts on Enter; a cell that holds a control hands the keyboard over on Enter or F2 and takes it back on Escape.
Give it a title
A grid is not named by the rows in it. Pass def.title and the grid points aria-labelledby at it.
Everything the grid draws for itself has a name
The column chooser, the select-all box, each row’s checkbox and expander, each column’s menu and each filter input are named after what they act on — “Select row 4”, “Filter Country”, “Column options for Age” — rather than after the icon drawn on them.
A column can be resized without a mouse
Each resizer is a role="separator" — APG’s window splitter — with the column’s width in pixels on aria-valuenow, so a screen reader reads the new width out as it changes.
KeyWhat happens
TabEnters the grid at one cell — not at every cell. Shift+Tab leaves it.
→ / ←One cell along the row. At either end, focus stays where it is.
↓ / ↑One row, keeping the column — through a group row or a detail panel with fewer cells, and through a grouped header whose cells cover several columns each.
Home / EndThe first or last cell of the row.
Ctrl + Home / EndThe first or last cell of the whole grid, scrolling there if it has not been rendered yet.
PageDown / PageUpA screenful of rows at a time.
Enter / SpaceSorts, on a sortable column header. Anywhere else, steps into the cell’s own control.
F2Steps into the cell’s control even on a header, where Enter is spoken for by the sort.
EscapeHands the keyboard back from that control to the cell.
KeyWhat happens
Tab / F2Reaches the resizer of the header cell focus is on. Escape hands the keyboard back to the cell.
→ / ←Moves the separator 16px, which widens or narrows the column exactly as dragging it would.
Home / EndThe narrowest the grid allows, or as wide as the grid itself.
Basic DataGrid
JSX
<DataGrid
  data={data}
  def={{
    columns: [
      { key: 'first_name', header: 'First name' },
      { key: 'last_name', header: 'Last name' },
      {
        key: 'age',
        header: 'Age',
        width: 90,
        align: 'right',
        Cell: ({ cell }) => {
          // better to define this function outside to avoid re-creation on each render
          return (
            <Flex
              bgColor="violet-50"
              height="fit"
              width="fit"
              ai="center"
              jc="center"
              overflow="hidden"
              className="parent"
              theme={{ dark: { bgColor: 'violet-600' } }}
            >
              <Box
                px={4}
                textOverflow="ellipsis"
                overflow="hidden"
                textWrap="nowrap"
                color="violet-700"
                fontWeight={600}
                hoverGroup={{ parent: { rotate: 180 } }}
                theme={{ dark: { color: 'violet-300' } }}
              >
                {cell.row.data.age}
              </Box>
            </Flex>
          );
        },
      },
      { key: 'email', header: 'Email', width: 300 },
      { key: 'street_address' },
      { key: 'city' },
      { key: 'country' },
      { key: 'favorite_color' },
      { key: 'gender' },
      { key: 'ssn' },
      { key: 'birthdate' },
      { key: 'phone_number' },
      { key: 'username' },
      { key: 'credit_card_number' },
      { key: 'salary' },
      { key: 'company_name' },
      { key: 'language' },
      { key: 'currency_code' },
    ],
    rowHeight: 40,
    visibleRowsCount: 5,
  }}
/>
DataGrid with Global Filter and Column Filters
JSX
<DataGrid
  data={data}
  def={{
    columns: [
      { key: 'first_name', header: 'First name', filterable: true },
      { key: 'last_name', header: 'Last name', filterable: true },
      { key: 'age', header: 'Age', width: 120, align: 'right', filterable: { type: 'number' } },
      { key: 'email', header: 'Email', width: 300, filterable: true },
      { key: 'country', filterable: { type: 'multiselect' } },
      { key: 'gender', filterable: { type: 'multiselect' } },
    ],
    rowHeight: 40,
    visibleRowsCount: 8,
    topBar: true,
    bottomBar: true,
    globalFilter: true,
  }}
/>
Grouped Columns with Row Selection
JSX
<DataGrid
  data={data}
  def={{
    columns: [
      {
        key: 'person',
        header: 'Person',
        columns: [
          { key: 'first_name', header: 'First name' },
          { key: 'last_name', header: 'Last name' },
        ],
      },
      {
        key: 'contact',
        header: 'Contact',
        columns: [
          { key: 'email', header: 'Email', width: 300 },
          { key: 'phone_number', header: 'Phone' },
        ],
      },
      { key: 'country' },
      { key: 'city' },
    ],
    rowSelection: { pinned: true },
    showRowNumber: { pinned: true },
  }}
/>
Row Detail — Orders with Items
JSX
// Define custom component style trees that extend 'datagrid' — 'orders-datagrid' for the outer
// grid and 'subgrid' for the one inside the detail row. Both names need the same .d.ts
// augmentation every Box.components() entry does; the Theme Setup page shows it.
// isExpanded/isExpandedFirstLeaf/isExpandedLastLeaf variants let you style
// the expanded row's cells individually (e.g. top + side borders).
// detailRow.content gets left/right borders without causing scroll overflow.
Box.components({
  subgrid: { extends: 'datagrid', styles: { b: 0, borderRadius: 0, shadow: 'none' } },
  'orders-datagrid': {
    extends: 'datagrid',
    children: {
      body: {
        children: {
          cell: {
            variants: {
              isExpanded: { bt: 3, bb: 0, bgColor: 'indigo-50', borderColor: 'indigo-300' },
              isExpandedFirstLeaf: { bl: 3 },
              isExpandedLastLeaf:  { br: 3 },
            },
          },
          detailRow: {
            styles: { bb: 3, bt: 0, bgColor: 'indigo-50', borderColor: 'indigo-300' },
            children: {
              content: { styles: { bl: 3, br: 3, borderColor: 'indigo-300' } },
            },
          },
        },
      },
    },
  },
});

<DataGrid
  component="orders-datagrid"
  data={orders}
  def={{
    rowKey: 'orderId',
    topBar: true,
    bottomBar: true,
    title: 'Orders',
    columns: [
      { key: 'orderId', header: 'Order #', width: 100, flexible: false },
      { key: 'customer', header: 'Customer' },
      { key: 'date', header: 'Date', width: 120 },
      { key: 'status', header: 'Status', width: 120 },
      { key: 'total', header: 'Total', width: 100, align: 'right' },
    ],
    rowDetail: {
      content: (order) => (
        <DataGrid
          component="subgrid"
          data={order.items}
          def={{
            columns: [
              { key: 'product', header: 'Product' },
              { key: 'qty', header: 'Qty', width: 80, align: 'right' },
              { key: 'price', header: 'Price', width: 100, align: 'right' },
            ],
            visibleRowsCount: 'all',
            rowHeight: 36,
          }}
        />
      ),
      pinned: true,
      expandOnRowClick: true,
    },
  }}
/>
Disable Sorting and Resizing
JSX
<DataGrid
  data={data}
  def={{
    columns: [
      { key: 'first_name', header: 'First name' },
      { key: 'last_name', header: 'Last name' },
      { key: 'age', header: 'Age', width: 100, sortable: true }, // Override: sortable
      { key: 'email', header: 'Email', width: 300, resizable: true }, // Override: resizable
      { key: 'country' },
      { key: 'city' },
    ],
    rowHeight: 40,
    visibleRowsCount: 5,
    sortable: false,   // Disable sorting globally
    resizable: false,  // Disable resizing globally
  }}
/>
Context Menu Control
JSX
<DataGrid
  data={data}
  def={{
    columns: [
      { key: 'first_name', header: 'First name' },
      { key: 'last_name', header: 'Last name' },
      { key: 'age', header: 'Age', width: 100, contextMenu: false }, // No context menu
      { key: 'email', header: 'Email', width: 300, contextMenu: { sort: true, pin: false, group: false } },
      { key: 'country' },
      { key: 'city' },
    ],
    rowHeight: 40,
    visibleRowsCount: 5,
    contextMenu: { sort: true, pin: true, group: false }, // Disable grouping globally
  }}
/>
Resizer Style
JSX
// 'hover' — resizer appears only when hovering the header cell
<DataGrid
  data={data}
  def={{
    columns: [
      { key: 'first_name', header: 'First name' },
      { key: 'last_name', header: 'Last name' },
      { key: 'age', header: 'Age', width: 100 },
      { key: 'email', header: 'Email', width: 300 },
      { key: 'country' },
      { key: 'city' },
    ],
    rowHeight: 40,
    visibleRowsCount: 5,
    resizerStyle: 'hover', // 'visible' | 'hover' | 'hidden'
  }}
/>
Resize Mode
JSX
// 'smooth' (default): width updates batched to one per animation frame (~60fps, ~1 frame
// behind the cursor). 'instant': width updates synchronously on every pointer move, so the
// column tracks the cursor with no added latency. Drag a column edge and toggle to compare.
<DataGrid
  data={data}
  def={{
    columns: [
      { key: 'first_name', header: 'First name' },
      { key: 'last_name', header: 'Last name' },
      { key: 'age', header: 'Age', width: 100 },
      { key: 'email', header: 'Email', width: 300 },
      { key: 'country' },
      { key: 'city' },
    ],
    rowHeight: 40,
    visibleRowsCount: 5,
    resizeMode: 'smooth', // or 'instant'
  }}
/>