# Row Grouping Plugin

> Group rows by column values with expandable groups.

The Row Grouping plugin organizes your rows into collapsible hierarchical groups. Perfect for organizing data by category, department, status, or any other dimension—or even multiple dimensions for nested grouping.

## Installation

```ts
import '@toolbox-web/grid/features/grouping-rows';
```

## Basic Usage

The `groupOn` callback receives each row and should return an array representing the group path. For single-level grouping, return a one-element array. For multi-level grouping, return multiple elements (e.g., `['Region', 'Department']`).

#### TypeScript

```ts
import { queryGrid } from '@toolbox-web/grid';

const grid = queryGrid('tbw-grid');
grid.gridConfig = {
  columns: [
    { field: 'name', header: 'Employee' },
    { field: 'department', header: 'Department' },
    { field: 'salary', header: 'Salary', type: 'currency' }
  ],
  features: {
    groupingRows: {
      groupOn: (row) => [row.department],
      showRowCount: true,
      defaultExpanded: false,
    },
  },
};

grid.rows = employees;
```

#### React

```tsx
import '@toolbox-web/grid-react/features/grouping-rows';
import { DataGrid } from '@toolbox-web/grid-react';

function EmployeeGrid({ employees }) {
  return (
    <DataGrid
      rows={employees}
      columns={[
        { field: 'name', header: 'Employee' },
        { field: 'department', header: 'Department' },
        { field: 'salary', header: 'Salary', type: 'currency' }
      ]}
      groupingRows={{ groupOn: (row) => row.department, showRowCount: true }}
    />
  );
}
```

#### Vue

```html
<script setup>
import '@toolbox-web/grid-vue/features/grouping-rows';
import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';

const employees = [
  { name: 'Alice', department: 'Engineering', salary: 95000 },
  { name: 'Bob', department: 'Marketing', salary: 75000 },
  { name: 'Carol', department: 'Engineering', salary: 90000 },
];
</script>

<template>
  <TbwGrid :rows="employees" :grouping-rows="{ groupOn: (row) => row.department, showRowCount: true }">
    <TbwGridColumn field="name" header="Employee" />
    <TbwGridColumn field="department" header="Department" />
    <TbwGridColumn field="salary" header="Salary" type="currency" />
  </TbwGrid>
</template>
```

#### Angular

```typescript
// Feature import - enables the [groupingRows] input
import { GridGroupingRowsDirective } from '@toolbox-web/grid-angular/features/grouping-rows';
import { Component } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';
import type { ColumnConfig } from '@toolbox-web/grid';

@Component({
  selector: 'app-employee-grid',
  imports: [Grid, GridGroupingRowsDirective],
  template: `
    <tbw-grid
      [rows]="employees"
      [columns]="columns"
      [groupingRows]="groupingConfig"
      style="height: 400px; display: block;">
    </tbw-grid>
  `,
})
export class EmployeeGridComponent {
  employees = [/* employee data */];

  columns: ColumnConfig[] = [
    { field: 'name', header: 'Employee' },
    { field: 'department', header: 'Department' },
    { field: 'salary', header: 'Salary', type: 'currency' }
  ];

  groupingConfig = {
    groupOn: (row: any) => [row.department],
    showRowCount: true,
  };
}
```

## Demos

### Default Grouping

```ts
// GroupingRowsDefaultDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/grouping-rows';

const container = document.getElementById('grouping-rows-default-demo');
if (container) {
  const sampleData = [
    { id: 1, name: 'Alice', department: 'Engineering', salary: 95000 },
    { id: 2, name: 'Bob', department: 'Marketing', salary: 75000 },
    { id: 3, name: 'Carol', department: 'Engineering', salary: 105000 },
    { id: 4, name: 'Dan', department: 'Sales', salary: 85000 },
    { id: 5, name: 'Eve', department: 'Marketing', salary: 72000 },
    { id: 6, name: 'Frank', department: 'Engineering', salary: 98000 },
    { id: 7, name: 'Grace', department: 'Sales', salary: 88000 },
  ];
  const columns = [
    { field: 'id', header: 'ID', type: 'number' },
    { field: 'name', header: 'Name' },
    { field: 'department', header: 'Department' },
    { field: 'salary', header: 'Salary', type: 'number' },
  ];

  const grid = queryGrid('tbw-grid', container)!;

  function rebuild(opts: Record<string, unknown>) {
    const accordion = opts.accordion as boolean ?? false;
    const expandRaw = opts.defaultExpanded as string;
    const defaultExpanded = accordion && expandRaw === 'all' ? false
      : expandRaw === 'all' ? true
      : expandRaw === 'none' ? false
      : expandRaw;

    grid.gridConfig = {
      columns,
      features: {
        groupingRows: {
          animation: (opts.animation as string) ?? 'slide',
          groupOn: (row: { department: string }) => row.department,
          defaultExpanded,
          showRowCount: opts.showRowCount as boolean ?? true,
          indentWidth: (opts.indentWidth as number) ?? 20,
          fullWidth: true,
          accordion,
        } as any,
      },
    };
    grid.rows = sampleData;
  }

  rebuild({ animation: 'slide', defaultExpanded: 'none', showRowCount: true, accordion: false, indentWidth: 20 });

  container.addEventListener('control-change', ((e: CustomEvent) => {
    rebuild(e.detail.allValues);
  }) as EventListener);
}
```

### Custom Group Row Renderer

Use `groupRowRenderer` to take full control of the group row's contents. The renderer receives a `GroupRowRenderParams` object (`key`, `value`, `depth`, `rows`, `expanded`, `toggleExpand`) and can return an `HTMLElement`, an HTML string, or `void` to keep the default rendering. The framework adapters accept their native return types (`ReactNode`, `VNode`, or a component `Type`) and bridge them automatically.

#### TypeScript

```ts
grid.gridConfig = {
  features: {
    groupingRows: {
      groupOn: (row) => [row.department],
      groupRowRenderer: (params) => {
        const arrow = params.expanded ? '▼' : '▶';
        // The `group-toggle` class makes the element a click target for the
        // plugin's delegated `closest('.group-toggle')` toggle handler.
        return `<button type="button" class="group-toggle">${arrow} <strong>${params.value}</strong> (${params.rows.length})</button>`;
      },
    },
  },
};
```

#### React

```tsx
import '@toolbox-web/grid-react/features/grouping-rows';
import { DataGrid } from '@toolbox-web/grid-react';

<DataGrid
  rows={employees}
  columns={columns}
  groupingRows={{
    groupOn: (row) => row.department,
    groupRowRenderer: (params) => (
      <button type="button" onClick={params.toggleExpand}>
        {params.expanded ? '▼' : '▶'} <strong>{params.value}</strong> ({params.rows.length})
      </button>
    ),
  }}
/>
```

#### Vue

```html
<script setup>
import { h } from 'vue';
import '@toolbox-web/grid-vue/features/grouping-rows';
import { TbwGrid } from '@toolbox-web/grid-vue';

const groupingConfig = {
  groupOn: (row) => row.department,
  groupRowRenderer: (params) =>
    h('button', { type: 'button', onClick: params.toggleExpand }, [
      params.expanded ? '▼ ' : '▶ ',
      h('strong', params.value),
      ` (${params.rows.length})`,
    ]),
};
</script>

<template>
  <TbwGrid :rows="employees" :grouping-rows="groupingConfig">
    <!-- columns -->
  </TbwGrid>
</template>
```

#### Angular

Pass a component class — the adapter bridges it and binds the params as inputs.

```typescript
import { GridGroupingRowsDirective } from '@toolbox-web/grid-angular/features/grouping-rows';
import { Component, input } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';

@Component({
  selector: 'app-group-row',
  template: `
    <button type="button" (click)="toggleExpand()">
      {{ expanded() ? '▼' : '▶' }} <strong>{{ value() }}</strong> ({{ rows().length }})
    </button>
  `,
})
export class GroupRowComponent {
  key = input.required<string>();
  value = input.required<unknown>();
  depth = input.required<number>();
  rows = input.required<unknown[]>();
  expanded = input.required<boolean>();
  toggleExpand = input.required<() => void>();
}

@Component({
  selector: 'app-my-grid',
  imports: [Grid, GridGroupingRowsDirective],
  template: `
    <tbw-grid
      [rows]="employees"
      [columns]="columns"
      [groupingRows]="groupingConfig"
      style="height: 400px; display: block;">
    </tbw-grid>
  `,
})
export class MyGridComponent {
  groupingConfig = {
    groupOn: (row: any) => [row.department],
    groupRowRenderer: GroupRowComponent,
  };
}
```

## Configuration Options

See [`GroupingRowsConfig`](./interfaces/groupingrowsconfig/) for the full list of options and their defaults.

### Default Expanded Options

The `defaultExpanded` option controls which groups are expanded on initial render:

```ts
// Expand all groups
features: { groupingRows: { defaultExpanded: true, groupOn: ... } }

// Collapse all groups (default)
features: { groupingRows: { defaultExpanded: false, groupOn: ... } }

// Expand group at index 0
features: { groupingRows: { defaultExpanded: 0, groupOn: ... } }

// Expand group with specific key
features: { groupingRows: { defaultExpanded: 'Engineering', groupOn: ... } }

// Expand multiple groups by key
features: { groupingRows: { defaultExpanded: ['Engineering', 'Sales'], groupOn: ... } }
```

:::note
When using `accordion: true`, prefer `defaultExpanded` with a single value
(number or string) rather than `true` or an array with multiple values.
:::

### Animation Options

The `animation` option controls how grouped rows appear/disappear:

```ts
// Slide animation (default)
features: { groupingRows: { animation: 'slide', ... } }

// Fade animation
features: { groupingRows: { animation: 'fade', ... } }

// No animation
features: { groupingRows: { animation: false, ... } }
```

:::note
Animation respects the grid-level `animation.mode` setting.
:::

### Accordion Mode

In accordion mode, only one group can be expanded at a time. Expanding a group automatically collapses all sibling groups at the same depth level.

```ts
features: {
  groupingRows: {
    groupOn: (row) => row.department,
    accordion: true, // Only one group open at a time
  },
},
```

### Aggregators

```ts
// GroupingRowsWithAggregatorsDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';

import '@toolbox-web/grid/features/grouping-rows';

const container = document.getElementById('grouping-rows-with-aggregators-demo');
if (container) {
  // Sample data for grouping demos
  const sampleData = [
    { id: 1, name: 'Alice', department: 'Engineering', salary: 95000 },
    { id: 2, name: 'Bob', department: 'Marketing', salary: 75000 },
    { id: 3, name: 'Carol', department: 'Engineering', salary: 105000 },
    { id: 4, name: 'Dan', department: 'Sales', salary: 85000 },
    { id: 5, name: 'Eve', department: 'Marketing', salary: 72000 },
    { id: 6, name: 'Frank', department: 'Engineering', salary: 98000 },
    { id: 7, name: 'Grace', department: 'Sales', salary: 88000 },
  ];
  const columns = [
    { field: 'id', header: 'ID', type: 'number' },
    { field: 'name', header: 'Name' },
    { field: 'department', header: 'Department' },
    { field: 'salary', header: 'Salary', type: 'number' },
  ];

  const grid = queryGrid('tbw-grid', container);

      grid.gridConfig = {
        columns,
        features: {
          groupingRows: {
            groupOn: (row: { department: string }) => row.department,
            defaultExpanded: false,
            showRowCount: true,
            indentWidth: 20,
            aggregators: {
              salary: 'sum',
            },
          },
        },
      };
      grid.rows = sampleData;
}
```

Display aggregate values (sum, avg, count, min, max, first, last) in group headers. Works in both full-width and per-column rendering modes.

```ts
features: {
  groupingRows: {
    groupOn: (row) => row.department,
    aggregators: {
      salary: 'sum',    // Sum of salaries in each group
      bonus: 'avg',     // Average bonus
      id: 'count',      // Count of rows
    },
  },
},
```

**Built-in aggregators:**
- `sum` - Sum of numeric values
- `avg` - Average of numeric values
- `count` - Number of rows
- `min` - Minimum value
- `max` - Maximum value
- `first` - First row's value
- `last` - Last row's value

You can also provide a custom aggregator function:

```ts
aggregators: {
  salary: (rows, field) => {
    const total = rows.reduce((sum, r) => sum + r[field], 0);
    return `$${total.toLocaleString()}`;
  },
}
```

## Multi-Level Grouping

Return multiple values from `groupOn` to create nested group hierarchies. Row counts display correctly at every depth level.

```ts
// GroupingRowsMultiLevelDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/grouping-rows';

const container = document.getElementById('grouping-rows-multi-level-demo');
if (container) {
  const sampleData = [
    { id: 1, name: 'Alice', country: 'USA', department: 'Engineering', salary: 95000 },
    { id: 2, name: 'Bob', country: 'USA', department: 'Marketing', salary: 75000 },
    { id: 3, name: 'Carol', country: 'USA', department: 'Engineering', salary: 105000 },
    { id: 4, name: 'Dan', country: 'UK', department: 'Sales', salary: 85000 },
    { id: 5, name: 'Eve', country: 'UK', department: 'Marketing', salary: 72000 },
    { id: 6, name: 'Frank', country: 'USA', department: 'Sales', salary: 98000 },
    { id: 7, name: 'Grace', country: 'UK', department: 'Sales', salary: 88000 },
    { id: 8, name: 'Henry', country: 'Germany', department: 'Engineering', salary: 92000 },
    { id: 9, name: 'Ivy', country: 'Germany', department: 'Engineering', salary: 110000 },
    { id: 10, name: 'Jack', country: 'UK', department: 'Engineering', salary: 78000 },
  ];

  const grid = queryGrid('tbw-grid', container)!;
  grid.gridConfig = {
    columns: [
      { field: 'id', header: 'ID', type: 'number', width: 60 },
      { field: 'name', header: 'Name' },
      { field: 'country', header: 'Country' },
      { field: 'department', header: 'Department' },
      { field: 'salary', header: 'Salary', type: 'number', align: 'right' },
    ],
    features: {
      groupingRows: {
        groupOn: (row: { country: string; department: string }) => [row.country, row.department],
        defaultExpanded: true,
        showRowCount: true,
      } as any,
    },
  };
  grid.rows = sampleData;
}
```

```ts
features: {
  groupingRows: {
    groupOn: (row) => [row.region, row.department, row.team],
  },
},
```

## Programmatic API

See [`GroupingRowsPlugin`](./classes/groupingrowsplugin/) for the full list of methods.

```ts
const plugin = grid.getPluginByName('groupingRows');

plugin.expand('Engineering');
plugin.collapse('Engineering');
plugin.toggle('Engineering');
plugin.expandAll();
plugin.collapseAll();
plugin.isExpanded('Engineering');    // boolean
plugin.getExpandedGroups();          // string[]
plugin.setGroupOn((row) => [row.region, row.department]);
// Optionally pass an initial expansion that resolves against the *new* groups
// (issue #335). Avoids the stale-snapshot race when chaining setGroupOn → expandAll.
plugin.setGroupOn((row) => row.counterparty, true);
plugin.setGroupOn((row) => row.region, ['EMEA', 'AMER']);
plugin.refreshGroups();
```

## Styling

The row grouping plugin supports CSS custom properties for theming. Override these on `tbw-grid` or a parent container:

### CSS Custom Properties

| Property | Default | Description |
| --- | --- | --- |
| `--tbw-group-indent-width` | `1.25em` (~20px) | Indentation per group level |
| `--tbw-grouping-rows-bg` | `var(--tbw-color-panel-bg)` | Group row background |
| `--tbw-grouping-rows-bg-hover` | `var(--tbw-color-row-hover)` | Group row hover |
| `--tbw-grouping-rows-toggle-hover` | `var(--tbw-color-row-hover)` | Toggle button hover |
| `--tbw-grouping-rows-count-color` | `var(--tbw-color-fg-muted)` | Count badge color |
| `--tbw-grouping-rows-aggregate-color` | `var(--tbw-color-fg-muted)` | Aggregate value color |
| `--tbw-toggle-size` | `1.25em` | Toggle button size |
| `--tbw-font-size-xs` | `0.7857em` | Count text size |
| `--tbw-animation-duration` | `200ms` | Expand/collapse animation |
| `--tbw-animation-easing` | `ease-out` | Animation curve |

### Example

```css
tbw-grid {
  /* Custom row grouping styling */
  --tbw-group-indent-width: 1.5em; /* Wider indentation */
  --tbw-grouping-rows-bg: #e8f5e9;
  --tbw-grouping-rows-bg-hover: #c8e6c9;
  --tbw-grouping-rows-count-color: #388e3c;
}
```

### CSS Classes

The row grouping plugin uses these class names:

| Class | Element |
| --- | --- |
| `.group-row` | Group header row |
| `.group-toggle` | Expand/collapse button |
| `.group-label` | Group name and value |
| `.group-count` | Row count badge |
| `.group-aggregates` | Container for aggregate values |
| `.group-aggregate` | Individual aggregate value |
| `.tbw-group-slide-in` | Child row slide animation |
| `.tbw-group-fade-in` | Child row fade animation |
| `[data-group-depth="N"]` | Group nesting level (0-4) |
| `.tbw-row-expanded` | Applied to a `.group-row` while it is expanded. Use this for theming expanded group rows instead of `[aria-expanded="true"]`. |

## Events

| Event            | Detail                                         | Description                                  |
| ---------------- | ---------------------------------------------- | -------------------------------------------- |
| `group-toggle`   | `{ key, expanded, value, depth }`              | Fired when a group is expanded/collapsed     |
| `group-expand`   | `{ groupKey, groupPath }`                      | Fired when a pre-defined group is expanded   |
| `group-collapse` | `{ groupKey, groupPath }`                      | Fired when a pre-defined group is collapsed  |

## Server-Side Data

> **Requires:** `ServerSidePlugin` — [see Server-Side Plugin](/grid/plugins/server-side.md)

For server-side scenarios where group structure is fetched from the server and rows are loaded on demand, use `ServerSidePlugin` alongside Row Grouping. ServerSide returns group definitions as root data; when the user expands a group, the plugin fetches group rows via `getChildRows()`.

#### TypeScript

```ts
import '@toolbox-web/grid/features/grouping-rows';
import '@toolbox-web/grid/features/server-side';
import { queryGrid } from '@toolbox-web/grid';

const grid = queryGrid('tbw-grid');
grid.gridConfig = {
  columns: [
    { field: 'name', header: 'Employee' },
    { field: 'department', header: 'Department' },
    { field: 'salary', header: 'Salary', type: 'currency' },
  ],
  features: {
    serverSide: {
      pageSize: 50,
      dataSource: {
        async getRows(params) {
          // Return group definitions as root-level nodes
          const res = await fetch(`/api/groups?start=${params.startNode}&end=${params.endNode}`);
          const data = await res.json();
          return { rows: data.groups, totalNodeCount: data.total };
        },
        async getChildRows(params) {
          // Return rows for an expanded group
          const { groupKey } = params.context;
          const res = await fetch(`/api/groups/${groupKey}/rows`);
          return { rows: await res.json() };
        },
      },
    },
    groupingRows: true,
  },
};
```

#### React

```tsx
import '@toolbox-web/grid-react/features/grouping-rows';
import '@toolbox-web/grid-react/features/server-side';
import { DataGrid } from '@toolbox-web/grid-react';

const dataSource = {
  async getRows(params) {
    const res = await fetch(`/api/groups?start=${params.startNode}&end=${params.endNode}`);
    return res.json();
  },
  async getChildRows(params) {
    const { groupKey } = params.context;
    const res = await fetch(`/api/groups/${groupKey}/rows`);
    return { rows: await res.json() };
  },
};

function ServerGroupedGrid() {
  return (
    <DataGrid
      columns={[
        { field: 'name', header: 'Employee' },
        { field: 'department', header: 'Department' },
        { field: 'salary', header: 'Salary', type: 'currency' },
      ]}
      serverSide={{ pageSize: 50, dataSource }}
      groupingRows
    />
  );
}
```

#### Vue

```html
<script setup>
import '@toolbox-web/grid-vue/features/grouping-rows';
import '@toolbox-web/grid-vue/features/server-side';
import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';

const serverSideConfig = {
  pageSize: 50,
  dataSource: {
    async getRows(params) {
      const res = await fetch(`/api/groups?start=${params.startNode}&end=${params.endNode}`);
      return res.json();
    },
    async getChildRows(params) {
      const { groupKey } = params.context;
      const res = await fetch(`/api/groups/${groupKey}/rows`);
      return { rows: await res.json() };
    },
  },
};
</script>

<template>
  <TbwGrid :server-side="serverSideConfig" grouping-rows>
    <TbwGridColumn field="name" header="Employee" />
    <TbwGridColumn field="department" header="Department" />
    <TbwGridColumn field="salary" header="Salary" type="currency" />
  </TbwGrid>
</template>
```

#### Angular

```typescript
import { GridGroupingRowsDirective } from '@toolbox-web/grid-angular/features/grouping-rows';
import { GridServerSideDirective } from '@toolbox-web/grid-angular/features/server-side';
import { Component } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';
import type { ColumnConfig } from '@toolbox-web/grid';

@Component({
  selector: 'app-server-grouped-grid',
  imports: [Grid, GridGroupingRowsDirective, GridServerSideDirective],
  template: `
    <tbw-grid
      [columns]="columns"
      [serverSide]="serverSideConfig"
      [groupingRows]="true"
      style="height: 400px; display: block;">
    </tbw-grid>
  `,
})
export class ServerGroupedGridComponent {
  columns: ColumnConfig[] = [
    { field: 'name', header: 'Employee' },
    { field: 'department', header: 'Department' },
    { field: 'salary', header: 'Salary', type: 'currency' },
  ];

  serverSideConfig = {
    pageSize: 50,
    dataSource: {
      async getRows(params: any) {
        const res = await fetch(`/api/groups?start=${params.startNode}&end=${params.endNode}`);
        return res.json();
      },
      async getChildRows(params: any) {
        const { groupKey } = params.context;
        const res = await fetch(`/api/groups/${groupKey}/rows`);
        return { rows: await res.json() };
      },
    },
  };
}
```

**Data flow:**
1. ServerSide fetches block → broadcasts `datasource:data`
2. GroupingRowsPlugin claims data as pre-defined groups
3. On group expand → fires `datasource:fetch-children` with `{ source: 'grouping-rows', groupKey }`
4. ServerSide calls `getChildRows()` → broadcasts `datasource:children`
5. GroupingRowsPlugin receives rows and renders them under the group

:::note
Child rows are fetched as a single batch — no pagination. If a group has many rows, limit the response server-side.
:::

### Standalone Mode (Without ServerSide)

If `ServerSidePlugin` is not loaded, use the imperative API to provide groups and rows directly:

```ts
import '@toolbox-web/grid/features/grouping-rows';
import { queryGrid } from '@toolbox-web/grid';

const grid = await queryGrid('tbw-grid', true);
grid.gridConfig = {
  columns: [
    { field: 'name', header: 'Employee' },
    { field: 'department', header: 'Department' },
    { field: 'salary', header: 'Salary', type: 'currency' },
  ],
  features: {
    groupingRows: true,
  },
};

const grouping = grid.getPluginByName('groupingRows');

// Lazy-load rows when a group is expanded
grid.on('group-expand', async ({ groupKey }) => {
  grouping.setGroupLoading(groupKey, true);
  const rows = await fetchGroupRows(groupKey);
  grouping.setGroupRows(groupKey, rows); // also clears loading state
});

// Load groups asynchronously
const groups = await fetch('/api/groups').then(r => r.json());
grouping.setGroups(groups);
```

:::note
`setGroupRows()` automatically clears the loading indicator — no need to call `setGroupLoading(key, false)` afterwards.
:::

| Method | Description |
| --- | --- |
| `setGroups(groups)` | Replace groups with an external structure |
| `getGroups()` | Get the current group definitions |
| `setGroupRows(key, rows)` | Populate rows for an expanded group |
| `setGroupLoading(key, loading)` | Toggle loading indicator for a group |
| `clearGroupRows(key?)` | Clear cached rows (specific group or all) |

### Nested Pre-Defined Groups

Groups can be nested using the `children` property:

```ts
const groups: GroupDefinition[] = [
  {
    key: 'engineering',
    value: 'Engineering',
    rowCount: 150,
    children: [
      { key: 'frontend', value: 'Frontend', rowCount: 60 },
      { key: 'backend', value: 'Backend', rowCount: 90 },
    ],
  },
];
```

## Plugin Compatibility

:::caution[Incompatible Plugins]
The Row Grouping plugin **cannot** be used with the following plugins:

- **[Tree](/grid/plugins/tree.md)** — Both plugins transform the entire row model. Tree flattens nested hierarchies while Row Grouping groups flat rows with synthetic headers. Use one approach per grid.
- **[Pivot](/grid/plugins/pivot.md)** — Pivot creates its own aggregated row and column structure. Row grouping cannot be applied on top of pivot-generated rows.
:::

A development-mode warning is shown if incompatible plugins are loaded together.

## See Also

- **[Column Groups](../grouping-columns/)** — Group column headers
- **[Tree](/grid/plugins/tree.md)** — Hierarchical tree data
- **[Pivot](/grid/plugins/pivot.md)** — Pivot table with grouping
- **[Common Patterns](/grid/guides/common-patterns.md)** — Application recipes using grouping
- **[Plugins Overview](/grid/plugins.md)** — Plugin compatibility and combinations
