Skip to content

Column groups

Group your columns, using multiple levels of nested column headers, to better reflect the structure of your data.

Nested column headers

The NestedHeaders plugin allows you to create a nested headers structure by using the HTML colspan and rowspan attributes.

To create a header that spans multiple columns, its corresponding configuration array element should be provided as an object with label and colspan properties. The label property defines the header’s label, while the colspan property defines the number of columns that the header should cover.

To create a header that spans multiple header rows, add a rowspan property to that object. See Rowspan below.

Header configuration object

Each nestedHeaders entry is either a string label or an object with these properties:

PropertyTypeDescription
labelstringThe header’s text.
colspannumberColumns the header spans (an integer greater than 1); groups the columns it covers.
rowspannumberHeader rows the header spans (an integer greater than 1). See Rowspan.
headerClassNamestringOne or more space-separated CSS class names added to the header element (for example, 'htRight').
visibleWhenstringFor a header in a collapsible group, which collapse state keeps it visible: 'collapsed', 'expanded', or 'always'. See Choose which columns stay visible when collapsed.
columnDropModestringWhat the group does when a foreign column is moved into its span: 'adopt' (default) or 'split'. See Keep a group cohesive or let it split.
collapsiblebooleanWhether the group can be collapsed and expanded. You enable it through the collapsibleColumns option, not in the nestedHeaders object. See Collapsible headers.

For defaults and full details, see the nestedHeaders API reference.

Configuration

Example

TypeScript
/* file: app.component.ts */
import { Component } from '@angular/core';
import { GridSettings, HotTableModule} from '@handsontable/angular-wrapper';
@Component({
selector: 'app-example1',
template: `
<hot-table
[settings]="hotSettings!" [data]="hotData">
</hot-table>
`,
standalone: true,
imports: [HotTableModule],
})
export class AppComponent {
readonly hotData = [
['A1', 'B1', 'C1', 'D1', 'E1', 'F1', 'G1', 'H1', 'I1', 'J1'],
['A2', 'B2', 'C2', 'D2', 'E2', 'F2', 'G2', 'H2', 'I2', 'J2'],
['A3', 'B3', 'C3', 'D3', 'E3', 'F3', 'G3', 'H3', 'I3', 'J3'],
['A4', 'B4', 'C4', 'D4', 'E4', 'F4', 'G4', 'H4', 'I4', 'J4'],
['A5', 'B5', 'C5', 'D5', 'E5', 'F5', 'G5', 'H5', 'I5', 'J5'],
];
readonly hotSettings: GridSettings = {
colHeaders: true,
rowHeaders: true,
height: 'auto',
nestedHeaders: [
['A', { label: 'B', colspan: 8 }, 'C'],
['D', { label: 'E', colspan: 4 }, { label: 'F', colspan: 4 }, 'G'],
[
'H',
{ label: 'I', colspan: 2 },
{ label: 'J', colspan: 2 },
{ label: 'K', colspan: 2 },
{ label: 'L', colspan: 2 },
'M',
],
['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'],
],
autoWrapRow: true,
autoWrapCol: true,
};
}
/* end-file */
/* file: app.config.ts */
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { registerAllModules } from 'handsontable/registry';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
// register Handsontable's modules
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
{
provide: HOT_GLOBAL_CONFIG,
useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig,
},
],
};
/* end-file */
HTML
<div>
<app-example1></app-example1>
</div>

Rowspan

The rowspan property sets how many header rows a single header cell should cover. Use an integer greater than 1. Positions in lower rows that sit under that cell can use an empty string '' as a placeholder, but those placeholders are optional. Handsontable can infer covered slots when you omit them.

You can combine rowspan and colspan on the same header object. The same rules apply as for colspan only: a header cannot be wider than its parent in the hierarchy, and overlapping header definitions are not supported.

Configuration

nestedHeaders: [
[{ label: 'A', rowspan: 2 }, { label: 'B', colspan: 2 }],
['', 'C', 'D'],
];

Collapsible headers

The CollapsibleColumns plugin enables columns and their headers to be collapsed/expanded.

This plugin adds multi-column headers which have buttons. Clicking these buttons will collapse or expand all “child” headers, leaving the first one visible.

The NestedHeaders plugin needs to be enabled for this to work properly.

Configuration

To enable the Collapsible Columns plugin, either set the collapsibleColumns configuration option to:

  • true - this will enable the functionality for all multi-column headers, every column with the colspan attribute defined will be extended with the “expand/collapse” button
  • An array of objects containing information specifying which headers should have the “expand/collapse” buttons for example:
collapsibleColumns: [
{ row: -4, col: 1, collapsible: true }, // Add the button to the 4th-level header of the 1st column - counting from the first table row upwards.
{ row: -3, col: 5, collapsible: true }, // Add the button to the 3rd-level header of the 5th column - counting from the first table row upwards.
];

Example

TypeScript
/* file: app.component.ts */
import { Component } from '@angular/core';
import { GridSettings, HotTableModule} from '@handsontable/angular-wrapper';
@Component({
selector: 'app-example2',
template: `
<hot-table
[settings]="hotSettings!" [data]="hotData">
</hot-table>
`,
standalone: true,
imports: [HotTableModule],
})
export class AppComponent {
readonly hotData = [
['A1', 'B1', 'C1', 'D1', 'E1', 'F1', 'G1', 'H1', 'I1', 'J1'],
['A2', 'B2', 'C2', 'D2', 'E2', 'F2', 'G2', 'H2', 'I2', 'J2'],
['A3', 'B3', 'C3', 'D3', 'E3', 'F3', 'G3', 'H3', 'I3', 'J3'],
['A4', 'B4', 'C4', 'D4', 'E4', 'F4', 'G4', 'H4', 'I4', 'J4'],
['A5', 'B5', 'C5', 'D5', 'E5', 'F5', 'G5', 'H5', 'I5', 'J5'],
];
readonly hotSettings: GridSettings = {
colHeaders: true,
rowHeaders: true,
colWidths: 60,
height: 'auto',
nestedHeaders: [
['A', { label: 'B', colspan: 8 }, 'C'],
['D', { label: 'E', colspan: 4 }, { label: 'F', colspan: 4 }, 'G'],
[
'H',
{ label: 'I', colspan: 2 },
{ label: 'J', colspan: 2 },
{ label: 'K', colspan: 2 },
{ label: 'L', colspan: 2 },
'M',
],
['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'],
],
collapsibleColumns: [
{ row: -4, col: 1, collapsible: true },
{ row: -3, col: 1, collapsible: true },
{ row: -2, col: 1, collapsible: true },
{ row: -2, col: 3, collapsible: true },
],
autoWrapRow: true,
autoWrapCol: true,
};
}
/* end-file */
/* file: app.config.ts */
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { registerAllModules } from 'handsontable/registry';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
// register Handsontable's modules
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
{
provide: HOT_GLOBAL_CONFIG,
useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig,
},
],
};
/* end-file */
HTML
<div>
<app-example2></app-example2>
</div>

Choose which columns stay visible when collapsed

By default, collapsing a group leaves its first column visible. To choose which columns stay visible in each state, add the visibleWhen property to a header in the nestedHeaders configuration. It accepts three values:

  • 'collapsed' - the column is visible only while its group is collapsed (hidden while expanded).
  • 'expanded' - the column is visible only while its group is expanded (hidden while collapsed).
  • 'always' - the column is visible in both states.

Once a group uses visibleWhen on any of its headers, the headers you leave unmarked default to 'expanded' - they are hidden when the group collapses. So you only mark the column(s) you want to keep: tag one with 'always' (stays in both states) or 'collapsed' (a summary column that appears only on collapse). At least one column of a group always stays visible, so its collapse button is never lost.

In the example below, collapse the Q1 2025 group: the per-month columns (marked 'expanded') hide and the Total column ('collapsed') appears. Expanding the group reverses it.

TypeScript
/* file: app.component.ts */
import { Component } from '@angular/core';
import { GridSettings, HotTableModule} from '@handsontable/angular-wrapper';
@Component({
selector: 'app-example3',
template: `
<hot-table
[settings]="hotSettings!" [data]="hotData">
</hot-table>
`,
standalone: true,
imports: [HotTableModule],
})
export class AppComponent {
readonly hotData = [
['North America', 4200, 3800, 4500, 12500],
['Europe', 3100, 2900, 3300, 9300],
['Asia Pacific', 2600, 2400, 2800, 7800],
['Latin America', 1500, 1700, 1600, 4800],
['Middle East', 1200, 1300, 1450, 3950],
];
readonly hotSettings: GridSettings = {
colHeaders: true,
rowHeaders: true,
colWidths: 90,
height: 'auto',
nestedHeaders: [
['Region', { label: 'Q1 2025', colspan: 4 }],
[
'Region',
{ label: 'Jan', visibleWhen: 'expanded' },
{ label: 'Feb', visibleWhen: 'expanded' },
{ label: 'Mar', visibleWhen: 'expanded' },
{ label: 'Total', visibleWhen: 'collapsed' },
],
],
collapsibleColumns: true,
autoWrapRow: true,
autoWrapCol: true,
};
}
/* end-file */
/* file: app.config.ts */
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { registerAllModules } from 'handsontable/registry';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
// register Handsontable's modules
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
{
provide: HOT_GLOBAL_CONFIG,
useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig,
},
],
};
/* end-file */
HTML
<div>
<app-example3></app-example3>
</div>

A group whose headers use no visibleWhen markers keeps the default behavior - collapsing leaves its first column visible. The visibleWhen property applies only to headers within a collapsible group.

Moving grouped columns

When you enable the ManualColumnMove plugin, nested column headers follow their columns. Moving a column moves both its data and its header label, so the labels always describe the columns beneath them.

When a move separates the columns of a group, that group renders as more than one header. Each header covers a contiguous run of the group’s columns and repeats the group’s label. No move is blocked.

Moving an entire group relocates the group and its label as a single unit. The group can land between other groups.

When a group is both collapsible and collapsed:

  • A move that keeps the group’s columns together keeps the group collapsed and moves it to its new position.
  • A move that would separate the group’s columns expands the group first, so none of its columns stay hidden without a way to expand them.

Keep a group cohesive or let it split

By default, a group is cohesive. When you move a column into the group’s span, the group adopts that column as a member and stays a single header that grows to cover it.

Set columnDropMode: 'split' on the group’s header object to change this. A group in split mode does not adopt a foreign column - one that belongs to a different group - moved into its span. It keeps its identity and renders as several same-label banners around that column.

columnDropMode controls only whether a group absorbs columns from outside it. A group always reclaims its own columns: moving one of a group’s columns out and then back into the group’s span merges it into a single banner again, regardless of its columnDropMode.

Moving a column out of a group separates a group in either mode the same way: the group renders as more than one header, each repeating its label.

nestedHeaders: [
// "Q1 2025" is cohesive (the default); "Q2 2025" splits when a column moves into it.
[{ label: 'Q1 2025 (adopt mode)', colspan: 3 }, { label: 'Q2 2025 (split mode)', colspan: 3, columnDropMode: 'split' }],
['January', 'February', 'March', 'April', 'May', 'June'],
];

In the example below, both quarters group three months. Q1 2025 uses adopt mode (the default) and Q2 2025 uses split mode. Drag a month from Q2 2025 into the middle of Q1 2025: the Q1 2025 group adopts it and spans four columns. Drag a month from Q1 2025 into the middle of Q2 2025: the Q2 2025 group splits into two Q2 2025 banners around the inserted column. Drag a Q2 2025 month out and then back into Q2 2025: it rejoins the single banner, because it belongs to that group.

TypeScript
/* file: app.component.ts */
import { Component } from '@angular/core';
import { GridSettings, HotTableModule} from '@handsontable/angular-wrapper';
@Component({
selector: 'app-example4',
template: `
<hot-table
[settings]="hotSettings!" [data]="hotData">
</hot-table>
`,
standalone: true,
imports: [HotTableModule],
})
export class AppComponent {
readonly hotData = [
['$4.2M', '$3.8M', '$4.5M', '$4.1M', '$4.7M', '$5.2M'],
['$3.1M', '$2.9M', '$3.4M', '$3.6M', '$3.8M', '$4.0M'],
['$5.6M', '$5.9M', '$6.3M', '$6.1M', '$6.8M', '$7.2M'],
['$1.4M', '$1.6M', '$1.5M', '$1.7M', '$1.9M', '$2.1M'],
['$2.2M', '$2.0M', '$2.4M', '$2.5M', '$2.7M', '$2.9M'],
];
readonly hotSettings: GridSettings = {
colHeaders: true,
rowHeaders: ['North America', 'Europe', 'Asia Pacific', 'Latin America', 'Middle East'],
rowHeaderWidth: 120,
height: 'auto',
manualColumnMove: true,
nestedHeaders: [
// Q1 2025 is cohesive (the default); Q2 2025 opts into splitting.
[
{ label: 'Q1 2025 (adopt mode)', colspan: 3 },
{ label: 'Q2 2025 (split mode)', colspan: 3, columnDropMode: 'split' },
],
['January', 'February', 'March', 'April', 'May', 'June'],
],
autoWrapRow: true,
autoWrapCol: true,
};
}
/* end-file */
/* file: app.config.ts */
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { registerAllModules } from 'handsontable/registry';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
// register Handsontable's modules
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
{
provide: HOT_GLOBAL_CONFIG,
useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig,
},
],
};
/* end-file */
HTML
<div>
<app-example4></app-example4>
</div>

Known limitations

  • A column header can span up to 1000 columns, as the HTML table specification sets the limit of colspan to 1000.
  • A nested column header can’t be wider than its parent element (headers can’t overlap).
  • If rowspan is larger than the number of header rows below the cell, Handsontable clamps it to the remaining header levels.

This header-focused shortcut works only when a collapsible column group header is focused. Enable navigableHeaders: true to move focus onto headers with the arrow keys. For more details, see Keyboard navigation.

WindowsmacOSActionExcelSheets
EnterEnterCollapse or expand the selected column group

Configuration options

Core methods

Hooks

Plugins