Column Grouping Plugin
The Column Grouping plugin enables visual grouping of columns under shared headers.
Installation
Section titled “Installation”import '@toolbox-web/grid/features/grouping-columns';Basic Usage
Section titled “Basic Usage”There are three ways to define column groups, from most to least recommended:
Option 1: Feature config columnGroups (Recommended)
Section titled “Option 1: Feature config columnGroups (Recommended)”Define everything in one place — groups, renderers, and plugin behavior:
import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/grouping-columns';
const grid = queryGrid('tbw-grid');grid.gridConfig = { columns: [ { field: 'firstName', header: 'First Name' }, { field: 'lastName', header: 'Last Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, { field: 'title', header: 'Title' }, { field: 'salary', header: 'Salary' }, ], features: { groupingColumns: { columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'] }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], }, },};import '@toolbox-web/grid-react/features/grouping-columns';import { DataGrid } from '@toolbox-web/grid-react';
function MyGrid({ data }) { return ( <DataGrid rows={data} columns={[ { field: 'firstName', header: 'First Name' }, { field: 'lastName', header: 'Last Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, { field: 'title', header: 'Title' }, { field: 'salary', header: 'Salary' }, ]} groupingColumns={{ columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'] }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], }} style={{ height: '400px' }} /> );}<script setup>import '@toolbox-web/grid-vue/features/grouping-columns';import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';
const data = [...];</script>
<template> <TbwGrid :rows="data" :grouping-columns="{ columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'] }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], }" style="height: 400px" > <TbwGridColumn field="firstName" header="First Name" /> <TbwGridColumn field="lastName" header="Last Name" /> <TbwGridColumn field="email" header="Email" /> <TbwGridColumn field="department" header="Department" /> <TbwGridColumn field="title" header="Title" /> <TbwGridColumn field="salary" header="Salary" /> </TbwGrid></template>import '@toolbox-web/grid-angular/features/grouping-columns';
import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { ColumnConfig } from '@toolbox-web/grid';
@Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [columns]="columns" [groupingColumns]="{ columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'] }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ] }" style="height: 400px; display: block;"> </tbw-grid> `,})export class MyGridComponent { rows = [...];
columns: ColumnConfig[] = [ { field: 'firstName', header: 'First Name' }, { field: 'lastName', header: 'Last Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, { field: 'title', header: 'Title' }, { field: 'salary', header: 'Salary' }, ];}Option 2: Grid config columnGroups
Section titled “Option 2: Grid config columnGroups”If your groups are part of a server-provided layout or shared data model, define them on gridConfig.columnGroups:
grid.gridConfig = { columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'] }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], columns: [...], features: { groupingColumns: true },};Option 3: Inline group property
Section titled “Option 3: Inline group property”For simple cases, assign group membership directly on each column. The group string becomes the group id:
grid.gridConfig = { columns: [ { field: 'firstName', header: 'First Name', group: 'personal' }, { field: 'lastName', header: 'Last Name', group: 'personal' }, { field: 'department', header: 'Department', group: 'work' }, ], features: { groupingColumns: true },};You can also pass an object with an explicit label:
{ field: 'firstName', header: 'First Name', group: { id: 'personal', label: 'Personal Info' } }Default Column Groups
Section titled “Default Column Groups”<tbw-grid style="height: 350px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/grouping-columns';
const firstNames = ['Alice', 'Bob', 'Carol', 'David', 'Emma', 'Frank', 'Grace', 'Henry', 'Ivy', 'Jack'];const lastNames = ['Johnson', 'Smith', 'Williams', 'Brown', 'Davis', 'Miller', 'Wilson', 'Moore', 'Taylor', 'Anderson'];const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance'];const titlesByDept = { Engineering: ['Senior Engineer', 'Software Engineer', 'Lead Engineer', 'DevOps Engineer', 'QA Engineer'], Marketing: ['Marketing Manager', 'Content Writer', 'Brand Manager', 'SEO Specialist'], Sales: ['Sales Rep', 'Sales Manager', 'Account Executive'], HR: ['HR Manager', 'Recruiter', 'Training Coordinator'], Finance: ['Accountant', 'Financial Analyst', 'Controller'],};function generateData(count: number) { return Array.from({ length: count }, (_, i) => { const firstName = firstNames[i % firstNames.length]; const lastName = lastNames[i % lastNames.length]; const department = departments[i % departments.length]; const titles = titlesByDept[department]; const title = titles[i % titles.length]; return { id: i + 1, firstName, lastName, email: `${firstName.toLowerCase()}@example.com`, department, title, salary: 50000 + Math.floor((i * 3456) % 70000), }; });}
const sampleData = generateData(20);const columns = [ { field: 'id', header: 'ID', type: 'number' }, { field: 'firstName', header: 'First Name', group: { id: 'personal', label: 'Personal Info' } }, { field: 'lastName', header: 'Last Name', group: { id: 'personal', label: 'Personal Info' } }, { field: 'email', header: 'Email', group: { id: 'personal', label: 'Personal Info' } }, { field: 'department', header: 'Department', group: { id: 'work', label: 'Work Info' } }, { field: 'title', header: 'Title', group: { id: 'work', label: 'Work Info' } }, { field: 'salary', header: 'Salary', type: 'number', group: { id: 'work', label: 'Work Info' } },];
const grid = queryGrid('tbw-grid')!;
function rebuild(showGroupBorders = true) { grid.gridConfig = { columns, features: { groupingColumns: { showGroupBorders } }, }; grid.rows = sampleData;}
rebuild();Custom Group Header Renderer
Section titled “Custom Group Header Renderer”Use groupHeaderRenderer to customize the content of group header cells. The renderer is called once per group, receives a GroupHeaderRenderParams object (including the group id), and can return an HTMLElement, an HTML string, or void to keep the default label.
Since the renderer receives params.id, a single function can differentiate rendering per group:
grid.gridConfig = { columns: [...], features: { groupingColumns: { columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'] }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], groupHeaderRenderer: (params) => { const icons: Record<string, string> = { 'personal-info': '👤', 'work-info': '💼', }; const icon = icons[params.id] ?? '📁'; return `${icon} <strong>${params.label}</strong> (${params.columns.length})`; }, }, },};import '@toolbox-web/grid-react/features/grouping-columns';import { DataGrid } from '@toolbox-web/grid-react';
const icons: Record<string, string> = { 'personal-info': '👤', 'work-info': '💼',};
<DataGrid rows={data} columns={columns} groupingColumns={{ columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'] }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], groupHeaderRenderer: (params) => ( <span> {icons[params.id] ?? '📁'} <strong>{params.label}</strong> ({params.columns.length}) </span> ), }}/><script setup>import { h } from 'vue';import '@toolbox-web/grid-vue/features/grouping-columns';import { TbwGrid } from '@toolbox-web/grid-vue';
const icons: Record<string, string> = { 'personal-info': '👤', 'work-info': '💼',};
const groupingConfig = { columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'] }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], groupHeaderRenderer: (params) => h('span', [ `${icons[params.id] ?? '📁'} `, h('strong', params.label), ` (${params.columns.length})`, ]),};</script>
<template> <TbwGrid :rows="data" :grouping-columns="groupingConfig"> <!-- columns --> </TbwGrid></template>You can pass a component class as the renderer. The adapter bridges it automatically.
import '@toolbox-web/grid-angular/features/grouping-columns';
import { Component, input } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { ColumnConfig } from '@toolbox-web/grid';
// Group header component — receives params as inputs@Component({ selector: 'app-group-header', template: `{{ icon() }} <strong>{{ label() }}</strong> ({{ columns().length }})`,})export class GroupHeaderComponent { id = input.required<string>(); label = input.required<string>(); columns = input.required<ColumnConfig[]>(); firstIndex = input.required<number>(); isImplicit = input.required<boolean>();
private icons: Record<string, string> = { 'personal-info': '👤', 'work-info': '💼', };
icon = () => this.icons[this.id()] ?? '📁';}
@Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [columns]="columns" [groupingColumns]="groupingConfig" style="height: 400px; display: block;"> </tbw-grid> `,})export class MyGridComponent { rows = [...]; columns = [...];
groupingConfig = { columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'] }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], groupHeaderRenderer: GroupHeaderComponent, };}You can also define a renderer directly on individual group definitions. A per-group renderer takes precedence over groupHeaderRenderer for that specific group:
grid.gridConfig = { columns: [...], features: { groupingColumns: { columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'], renderer: (params) => `👤 <strong>${params.label}</strong>`, }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], // Fallback for groups without their own renderer (Work Info in this case) groupHeaderRenderer: (params) => `<strong>${params.label}</strong>`, }, },};import '@toolbox-web/grid-react/features/grouping-columns';import { DataGrid } from '@toolbox-web/grid-react';
<DataGrid rows={data} columns={columns} groupingColumns={{ columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'], renderer: (params) => ( <span>👤 <strong>{params.label}</strong></span> ), }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], // Fallback for groups without their own renderer (Work Info in this case) groupHeaderRenderer: (params) => <strong>{params.label}</strong>, }}/><script setup>import { h } from 'vue';import '@toolbox-web/grid-vue/features/grouping-columns';import { TbwGrid } from '@toolbox-web/grid-vue';
const groupingConfig = { columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'], renderer: (params) => h('span', ['👤 ', h('strong', params.label)]), }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], // Fallback for groups without their own renderer (Work Info in this case) groupHeaderRenderer: (params) => h('strong', params.label),};</script>
<template> <TbwGrid :rows="data" :grouping-columns="groupingConfig"> <!-- columns --> </TbwGrid></template>Per-group renderers can also be component classes:
import '@toolbox-web/grid-angular/features/grouping-columns';
import { Component, input } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { ColumnConfig } from '@toolbox-web/grid';
@Component({ selector: 'app-personal-group-header', template: `👤 <strong>{{ label() }}</strong>`,})export class PersonalGroupHeaderComponent { id = input.required<string>(); label = input.required<string>(); columns = input.required<ColumnConfig[]>(); firstIndex = input.required<number>(); isImplicit = input.required<boolean>();}
@Component({ selector: 'app-default-group-header', template: `<strong>{{ label() }}</strong>`,})export class DefaultGroupHeaderComponent { id = input.required<string>(); label = input.required<string>(); columns = input.required<ColumnConfig[]>(); firstIndex = input.required<number>(); isImplicit = input.required<boolean>();}
@Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [columns]="columns" [groupingColumns]="groupingConfig" style="height: 400px; display: block;"> </tbw-grid> `,})export class MyGridComponent { rows = [...]; columns = [...];
groupingConfig = { columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'], renderer: PersonalGroupHeaderComponent, }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], // Fallback for groups without their own renderer groupHeaderRenderer: DefaultGroupHeaderComponent, };}<tbw-grid style="height: 350px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/grouping-columns';
const firstNames = ['Alice', 'Bob', 'Carol', 'David', 'Emma', 'Frank', 'Grace', 'Henry', 'Ivy', 'Jack'];const lastNames = ['Johnson', 'Smith', 'Williams', 'Brown', 'Davis', 'Miller', 'Wilson', 'Moore', 'Taylor', 'Anderson'];const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance'];const titlesByDept: Record<string, string[]> = { Engineering: ['Senior Engineer', 'Software Engineer', 'Lead Engineer', 'DevOps Engineer', 'QA Engineer'], Marketing: ['Marketing Manager', 'Content Writer', 'Brand Manager', 'SEO Specialist'], Sales: ['Sales Rep', 'Sales Manager', 'Account Executive'], HR: ['HR Manager', 'Recruiter', 'Training Coordinator'], Finance: ['Accountant', 'Financial Analyst', 'Controller'],};
function generateData(count: number) { return Array.from({ length: count }, (_, i) => { const firstName = firstNames[i % firstNames.length]; const lastName = lastNames[i % lastNames.length]; const department = departments[i % departments.length]; const titles = titlesByDept[department]; const title = titles[i % titles.length]; return { id: i + 1, firstName, lastName, email: `${firstName.toLowerCase()}@example.com`, department, title, salary: 50000 + Math.floor((i * 3456) % 70000), }; });}
const sampleData = generateData(20);const columns = [ { field: 'id', header: 'ID', type: 'number' as const }, { field: 'firstName', header: 'First Name', group: { id: 'personal', label: 'Personal Info' } }, { field: 'lastName', header: 'Last Name', group: { id: 'personal', label: 'Personal Info' } }, { field: 'email', header: 'Email', group: { id: 'personal', label: 'Personal Info' } }, { field: 'department', header: 'Department', group: { id: 'work', label: 'Work Info' } }, { field: 'title', header: 'Title', group: { id: 'work', label: 'Work Info' } }, { field: 'salary', header: 'Salary', type: 'number' as const, group: { id: 'work', label: 'Work Info' } },];
const icons: Record<string, string> = { personal: '👤', work: '💼',};
const grid = queryGrid('tbw-grid')!;grid.gridConfig = { columns, features: { groupingColumns: { groupHeaderRenderer: (params) => { const icon = icons[params.id] ?? '📁'; const el = document.createElement('span'); el.style.cssText = 'display: flex; align-items: center; gap: 0.4em;'; el.innerHTML = `<span>${icon}</span> <strong>${params.label}</strong> <span style="opacity: 0.6; font-size: 0.85em;">(${params.columns.length} columns)</span>`; return el; }, }, },};grid.rows = sampleData;Configuration Options
Section titled “Configuration Options”Where to configure what
Section titled “Where to configure what”| What | Where | Notes |
|---|---|---|
| Group definitions | features.groupingColumns.columnGroups | Recommended — keeps everything in one place |
| Group definitions (alt) | gridConfig.columnGroups | Useful for server-driven layouts |
| Group membership | columns[].group | Simplest — just assigns a column to a group |
| Custom rendering | groupHeaderRenderer or per-group renderer | On the feature config or group definition |
| Plugin behavior | showGroupBorders, lockGroupOrder | On the feature config |
Type reference
Section titled “Type reference”- Plugin options:
GroupingColumnsConfig—columnGroups,groupHeaderRenderer,showGroupBorders,lockGroupOrder - Group definition:
ColumnGroupDefinition—id(optional),header,children,renderer - Renderer params:
GroupHeaderRenderParams—id,label,columns,firstIndex,isImplicit - Runtime group data:
ColumnGroup— computed group objects returned bygetGroups() - Column config
group:string(group ID) or{ id: string; label?: string }
Programmatic API
Section titled “Programmatic API”The plugin provides these methods on the plugin instance:
const plugin = grid.getPluginByName('groupingColumns');
// Check if grouping is activeplugin.isGroupingActive(); // boolean
// Get all computed groupsplugin.getGroups(); // ColumnGroup[]
// Get columns in a specific groupplugin.getGroupColumns('personal'); // ColumnConfig[]
// Force refresh of column groupsplugin.refresh();Usage with Column Reordering
Section titled “Usage with Column Reordering”When using GroupingColumnsPlugin together with the Reorder Columns plugin, column reordering does not enforce group boundaries by default. Users can drag columns out of their groups, which will cause the groups to be recomputed based on the new column order.
Built-in: lockGroupOrder
Section titled “Built-in: lockGroupOrder”Set lockGroupOrder: true to automatically prevent columns from being moved outside their group:
grid.gridConfig = { features: { groupingColumns: { lockGroupOrder: true }, },};This blocks moves that would break group contiguity in both header drag-and-drop and the visibility panel.
Manual: column-move Event
Section titled “Manual: column-move Event”For more control, the column-move event is cancelable, so you can implement custom validation:
grid.on('column-move', ({ field, fromIndex, toIndex, columnOrder }, e) => {
// Example: prevent moves that break group boundaries if (!isValidMoveWithinGroup(field, fromIndex, toIndex)) { e.preventDefault(); // Column snaps back to original position return; }
// Persist the new order saveColumnOrder(columnOrder);});The column-move event includes the complete columnOrder array, making it easy to persist user preferences.
Styling
Section titled “Styling”The column grouping plugin supports CSS custom properties for theming. Override these on tbw-grid or a parent container:
CSS Custom Properties
Section titled “CSS Custom Properties”| Property | Default | Description |
|---|---|---|
--tbw-grouping-columns-header-bg | var(--tbw-color-header-bg) | Group header row background |
--tbw-grouping-columns-border | var(--tbw-color-border) | Group borders |
--tbw-grouping-columns-separator | var(--tbw-color-border-strong) | Separator between groups |
--tbw-button-padding-sm | 0.25rem 0.5rem | Group header cell padding |
--tbw-font-size-sm | 0.9285em | Group header font size |
Example
Section titled “Example”tbw-grid { /* Custom column grouping styling */ --tbw-grouping-columns-header-bg: #e3f2fd; --tbw-grouping-columns-border: #90caf9; --tbw-grouping-columns-separator: #1976d2;}CSS Classes
Section titled “CSS Classes”The column grouping plugin uses these class names:
| Class | Element |
|---|---|
.header-group-row | Group header row container |
.header-group-row.no-borders | Borderless mode |
.header-group-cell | Individual group header cell |
.header-row .cell.grouped | Column under a group |
.header-row .cell.group-end | Last column in a group |
See Also
Section titled “See Also”- Row Grouping — Group rows by field values
- Pinned Columns — Sticky columns
- Reorder Columns — Drag-and-drop column reordering