

## Overview [#overview]

Rows are flat records by default and need no row-specific configuration. Add
these capabilities only when the data needs them:

| Capability | Use for                                          | Configure with                             |
| ---------- | ------------------------------------------------ | ------------------------------------------ |
| Grouping   | Group-by views and aggregated values.            | `enableGrouping`, `aggregationFn`          |
| Hierarchy  | Records that contain child records.              | `getSubRows`                               |
| Details    | Supplemental content that is not another record. | `getRowCanExpand`, `renderExpandedContent` |

Grouping creates summary rows, while hierarchy keeps parent rows as real data
records. Avoid combining dynamic grouping with hierarchical source data unless
the product-specific composition has been tested.

Row pinning is opt-in. Add pin actions where they make sense for the product and
use stable row IDs when pinning or expansion must survive data changes. Saved
views include grouping order; expansion, selection, and pinning are not included.
See [Saved Views](/components/data-grid/saved-views).

## Examples [#examples]

### Grouping and aggregation [#grouping-and-aggregation]

Enable grouping on relevant columns to add them to Group by, then use
`aggregationFn` and `aggregatedCell` for summaries. This example activates
group rows to expand them and selects leaf rows.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="DataGridGroupingScenario" />
  </Tab>

  <Tab value="Code">
    ```tsx
    columnHelper.accessor("status", {
      header: "Status",
      enableGrouping: true,
    });

    columnHelper.accessor("owner", {
      header: "Owner",
      enableGrouping: true,
    });

    columnHelper.accessor("amount", {
      header: "Value",
      enableGrouping: false,
      aggregationFn: "sum",
      aggregatedCell: ({ getValue }) => `$${getValue<number>().toLocaleString()} total`,
    });

    const table = useDataGridTable({
      columns,
      data,
      enableColumnPinning: false,
      enableHiding: false,
      enableRowSelection: (row) => !row.getIsGrouped(),
      enableSorting: false,
      initialState: { grouping: ["status"] },
    });

    <DataGrid
      getRowCanActivate={(row) => row.getIsGrouped()}
      interaction="row-selection"
      onRowActivate={({ row }) => row.toggleExpanded()}
      table={table}
    >
      <DataGrid.Viewport aria-label="Grouped matters" />
    </DataGrid>
    ```
  </Tab>
</Tabs>

### Custom group rows [#custom-group-rows]

Use `renderGroupedRowContent` when grouped rows need a full-width
presentation. The renderer receives the grouped row and table; Cubitt retains
the row structure and interaction behavior.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="DataGridCustomGroupRowsScenario" />
  </Tab>

  <Tab value="Code">
    ```tsx
    function GroupedMatterRow({
      row,
      table,
    }: DataGridGroupedRowContentContext<Matter>) {
      const columnId = row.groupingColumnId;
      if (!columnId) return null;

      const label =
        table.getColumn(columnId)?.columnDef.meta?.headerLabel ?? columnId;
      const count = row.getLeafRows().length;

      return (
        <div className="flex items-center gap-2">
          <DataGrid.RowExpander row={row} />
          <span>{label}</span>
          <strong>{String(row.groupingValue)}</strong>
          <span>{count} {count === 1 ? "matter" : "matters"}</span>
          <span className="ms-auto">
            {`$${row.getValue<number>("amount").toLocaleString()}`} total
          </span>
        </div>
      );
    }

    <DataGrid
      getRowCanActivate={(row) => row.getIsGrouped()}
      interaction="row-selection"
      onRowActivate={({ row }) => row.toggleExpanded()}
      table={table}
    >
      <DataGrid.Viewport
        aria-label="Grouped matters"
        renderGroupedRowContent={(context) => (
          <GroupedMatterRow {...context} />
        )}
      />
    </DataGrid>
    ```
  </Tab>
</Tabs>

### Hierarchical rows [#hierarchical-rows]

Return child records from `getSubRows` and place a `DataGrid.RowExpander` in the
identifying column.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="DataGridHierarchyScenario" />
  </Tab>

  <Tab value="Code">
    ```tsx
    type WorkItem = {
      id: string;
      name: string;
      status: string;
      children?: WorkItem[];
    };

    const columns = columnHelper.columns([
      columnHelper.accessor("name", {
        header: "Work item",
        meta: { rowHeader: true },
        cell: ({ getValue, row }) => (
          <div className="flex items-center gap-1">
            <DataGrid.RowExpander row={row} />
            <span>{getValue()}</span>
          </div>
        ),
      }),
    ]);

    const table = useDataGridTable(
      {
        columns,
        data: workItems,
        getRowId: (row) => row.id,
        getSubRows: (row) => row.children,
        initialState: { expanded: { "phase-discovery": true } },
      },
      () => null,
    );
    ```
  </Tab>
</Tabs>

### Expanded details [#expanded-details]

Use `getRowCanExpand` and `renderExpandedContent` when expansion reveals
supplemental content rather than child rows.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="DataGridDetailsScenario" />
  </Tab>

  <Tab value="Code">
    ```tsx
    columnHelper.accessor("name", {
      header: "Matter",
      meta: { rowHeader: true },
      cell: ({ getValue, row }) => (
        <div className="flex items-center gap-1">
          <DataGrid.RowExpander
            aria-controls={`matter-details-${row.id}`}
            label={`Toggle details for ${row.original.name}`}
            row={row}
          />
          <span>{getValue()}</span>
        </div>
      ),
    });

    const table = useDataGridTable(
      { columns, data: matters, getRowCanExpand: () => true, getRowId: (row) => row.id },
      () => null,
    );

    <DataGrid table={table}>
      <DataGrid.Viewport
        aria-label="Matters with details"
        renderExpandedContent={({ row }) => (
          <div id={`matter-details-${row.id}`}>{row.original.notes}</div>
        )}
      />
    </DataGrid>
    ```
  </Tab>
</Tabs>

### Row pinning [#row-pinning]

Enable row pinning and call `row.pin()` from consumer-defined actions.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="DataGridRowPinningScenario" />
  </Tab>

  <Tab value="Code">
    ```tsx
    columnHelper.display({
      id: "actions",
      header: () => <span className="sr-only">Actions</span>,
      cell: ({ row }) => {
        const label = row.getIsGrouped()
          ? String(row.groupingValue)
          : row.original.name;

        return (
          <Menu>
            <MenuTrigger
              render={
                <Button
                  aria-label={`Actions for ${label}`}
                  className="absolute inset-e-3.5 top-1/2 -translate-y-1/2"
                  mode="icon"
                  tooltip={`Actions for ${label}`}
                  variant="ghost"
                />
              }
            >
              <Dots />
            </MenuTrigger>
            <MenuContent align="end">
              <MenuCheckboxItem
                checked={row.getIsPinned() === "top"}
                onCheckedChange={(checked) => row.pin(checked ? "top" : false)}
              >
                <ArrowUpToLine />
                Pin to top
              </MenuCheckboxItem>
              <MenuCheckboxItem
                checked={row.getIsPinned() === "bottom"}
                onCheckedChange={(checked) => row.pin(checked ? "bottom" : false)}
              >
                <ArrowDownToLine />
                Pin to bottom
              </MenuCheckboxItem>
            </MenuContent>
          </Menu>
        );
      },
      enableCellSelection: false,
      enableGrouping: false,
      enableHiding: false,
      enablePinning: false,
      enableResizing: false,
      meta: { align: "end" },
      size: 64,
    });

    const table = useDataGridTable({
      columns,
      data: rowPinningMatters,
      enableRowPinning: true,
      getRowId: (row) => row.id,
    });

    <DataGrid table={table}>
      <DataGrid.Viewport
        aria-label="Pinned matter rows"
        className="max-h-96"
      />
    </DataGrid>
    ```
  </Tab>
</Tabs>

## API Reference [#api-reference]

### Table options [#table-options]

| Option               | Type                                            | Default            | Description                                                                             |
| -------------------- | ----------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------- |
| `enableGrouping`     | `boolean`                                       | `true`             | Enables grouping for eligible columns.                                                  |
| `groupedColumnMode`  | `"reorder" \| "remove" \| false`                | `"reorder"`        | Moves grouped columns to the start, removes them from display, or leaves them in place. |
| `onGroupingChange`   | `OnChangeFn<GroupingState>`                     | State updater      | Receives grouping updates when the consumer controls the slice.                         |
| `getRowId`           | `(row, index, parent?) => string`               | Index-derived      | Returns a stable ID for each row.                                                       |
| `getSubRows`         | `(row, index) => readonly TData[] \| undefined` | -                  | Returns child records for a hierarchical row.                                           |
| `getRowCanExpand`    | `(row) => boolean`                              | Rows with children | Overrides whether a row can expand.                                                     |
| `onExpandedChange`   | `OnChangeFn<ExpandedState>`                     | State updater      | Receives expansion updates when the consumer controls the slice.                        |
| `enableRowPinning`   | `boolean \| ((row) => boolean)`                 | `false`            | Enables pinning for every row or rows that match a predicate.                           |
| `keepPinnedRows`     | `boolean`                                       | `true`             | Keeps pinned rows rendered when filtering removes them from the centre row model.       |
| `onRowPinningChange` | `OnChangeFn<RowPinningState>`                   | State updater      | Receives row-pinning updates when the consumer controls the slice.                      |

### Column options [#column-options]

| Option             | Type                                           | Default             | Description                                  |
| ------------------ | ---------------------------------------------- | ------------------- | -------------------------------------------- |
| `enableGrouping`   | `boolean`                                      | `true`              | Allows the column to appear in Group by.     |
| `getGroupingValue` | `(record, index, row) => unknown`              | Column value        | Returns the value used to group the column.  |
| `aggregationFn`    | `AggregationFnOption<DataGridFeatures, TData>` | `"auto"`            | Calculates the grouped value for the column. |
| `aggregatedCell`   | `(context) => ReactNode`                       | Formatted aggregate | Renders the grouped value.                   |

### Row and column methods [#row-and-column-methods]

| API                     | Signature                                                                              | Description                                       |
| ----------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------- |
| `column.toggleGrouping` | `() => void`                                                                           | Adds or removes the column from grouping.         |
| `column.getIsGrouped`   | `() => boolean`                                                                        | Returns whether the column is currently grouped.  |
| `row.getIsGrouped`      | `() => boolean`                                                                        | Returns whether the row is a synthetic group row. |
| `row.getCanExpand`      | `() => boolean`                                                                        | Returns whether the row can expand.               |
| `row.getIsExpanded`     | `() => boolean`                                                                        | Returns whether the row is expanded.              |
| `row.toggleExpanded`    | `(expanded?: boolean) => void`                                                         | Toggles or sets the row's expanded state.         |
| `row.getCanPin`         | `() => boolean`                                                                        | Returns whether the row can be pinned.            |
| `row.getIsPinned`       | `() => "top" \| "bottom" \| false`                                                     | Returns the row's current pinned position.        |
| `row.pin`               | `(position: "top" \| "bottom" \| false, includeLeafRows?, includeParentRows?) => void` | Pins the row or returns it to the centre.         |

### DataGrid.Viewport [#datagridviewport]

| Prop                      | Type                     | Default | Description                                                                           |
| ------------------------- | ------------------------ | ------- | ------------------------------------------------------------------------------------- |
| `renderExpandedContent`   | `(context) => ReactNode` | -       | Renders supplemental content below an expanded row.                                   |
| `renderGroupedRowContent` | `(context) => ReactNode` | -       | Replaces each synthetic grouped row's generated cells with one full-width row header. |

### DataGrid.RowExpander [#datagridrowexpander]

DataGrid adds this automatically to grouped rows. Use it directly for
hierarchical rows and expanded details.

| Prop            | Type                           | Default             | Description                                |
| --------------- | ------------------------------ | ------------------- | ------------------------------------------ |
| `row`           | `Row<DataGridFeatures, TData>` | -                   | Row controlled by the expander. Required.  |
| `label`         | `string`                       | `"Toggle {row.id}"` | Accessible label for the expansion button. |
| `aria-controls` | `string`                       | -                   | ID controlled by the expansion button.     |
| `className`     | `string`                       | -                   | Additional layout classes for the wrapper. |

### DataGridExpandedContentContext [#datagridexpandedcontentcontext]

The `renderExpandedContent` viewport prop receives this context.

| Field   | Type                           | Description     |
| ------- | ------------------------------ | --------------- |
| `row`   | `Row<DataGridFeatures, TData>` | Expanded row.   |
| `table` | `DataGridTable<TData>`         | Table instance. |

### DataGridGroupedRowContentContext [#datagridgroupedrowcontentcontext]

The `renderGroupedRowContent` viewport prop receives this context. Read group
identity from `row.groupingColumnId` and `row.groupingValue`; `row.original`
belongs to a leaf record.

| Field   | Type                           | Description            |
| ------- | ------------------------------ | ---------------------- |
| `row`   | `Row<DataGridFeatures, TData>` | Synthetic grouped row. |
| `table` | `DataGridTable<TData>`         | Table instance.        |
