Skip to content

Multi Row Headers

The Multi Row Header plugin renders banded, multi-level headers from the same nested ColumnGrouping structure RevoGrid already uses for grouped columns. Use it when shallow leaf columns should fill the unused header depth while deeper branches continue to show stacked group bands.

This is the header-focused feature to use for AG Grid or DevExtreme-style multi-level headers. It is separate from CellMergePlugin, which only merges body and pinned data cells.

Source code
TypeScriptts
import { defineCustomElements } from '@revolist/revogrid/loader';
import {
  AdvanceFilterPlugin,
  ColumnCollapsePlugin,
  ColumnStretchPlugin,
  MultiRowHeaderPlugin,
  RowOddPlugin,
} from '@revolist/revogrid-pro';
import {
  multiRowHeaderColumns,
  multiRowHeaderRows,
} from './multi-row-header.shared';
import { currentTheme } from '../composables/useRandomData';
import './MultiRowHeader.scss';

defineCustomElements();
const { isDark } = currentTheme();

export function load(parentSelector: string): () => void {
  const parent = document.querySelector(parentSelector);
  if (!parent) {
    return () => undefined;
  }

  const wrapper = document.createElement('div');
  const darkTheme = isDark();
  wrapper.className = `multi-row-header-example flex grow flex-col${darkTheme ? ' multi-row-header-example--dark' : ''}`;

  const grid = document.createElement('revo-grid');
  grid.columns = multiRowHeaderColumns;
  grid.plugins = [
    MultiRowHeaderPlugin,
    ColumnCollapsePlugin,
    AdvanceFilterPlugin,
    RowOddPlugin,
    ColumnStretchPlugin,
  ];
  grid.multiRowHeader = true;
  grid.filter = true;
  grid.stretch = 'last';
  grid.theme = darkTheme ? 'darkMaterial' : 'material';
  grid.hideAttribution = true;

  wrapper.append(grid);
  parent.append(wrapper);
  grid.source = multiRowHeaderRows;

  return () => {
    wrapper.remove();
  };
}
Shared columnsts
import type { DataType } from '@revolist/revogrid';

const bandClass = (tone: 'green' | 'blue' | 'muted') => () => ({
  class: {
    [`multi-row-header-band-${tone}`]: true,
  },
});

export const multiRowHeaderColumns = [
  {
    name: 'Group A',
    columnProperties: bandClass('muted'),
    children: [
      { name: 'Athlete', prop: 'athlete', size: 180, filter: true },
      {
        name: 'Group B',
        collapsible: true,
        columnProperties: bandClass('muted'),
        children: [
          { name: 'Country', prop: 'country', size: 160, filter: true },
          {
            name: 'Group C',
            columnProperties: bandClass('green'),
            children: [
              { name: 'Sport', prop: 'sport', size: 145, filter: true },
              {
                name: 'Group D',
                columnProperties: bandClass('green'),
                children: [
                  { name: 'Total', prop: 'total', size: 140, filter: true },
                  {
                    name: 'Group E',
                    collapsible: true,
                    columnProperties: bandClass('green'),
                    children: [
                      { name: 'Gold', prop: 'gold', size: 130, filter: true },
                      {
                        name: 'Group F',
                        collapsible: true,
                        columnProperties: bandClass('green'),
                        children: [
                          { name: 'Silver', prop: 'silver', size: 130, filter: true },
                          {
                            name: 'Group G',
                            columnProperties: bandClass('blue'),
                            children: [
                              { name: 'Bronze', prop: 'bronze', size: 140, filter: true },
                            ],
                          },
                          { name: 'Rank', prop: 'rank', filter: true },
                        ],
                      },
                    ],
                  },
                ],
              },
            ],
          },
        ],
      },
    ],
  },
  {
    name: 'Pinned Totals',
    columnProperties: bandClass('muted'),
    children: [
      { name: 'Score', prop: 'score', pin: 'colPinEnd', size: 112, filter: true },
    ],
  },
];

export const balancedMultiRowHeaderColumns = [
  {
    name: 'Balanced',
    children: [
      {
        name: 'Flat Leaf',
        prop: 'flat',
        size: 160,
        suppressSpanHeaderHeight: true,
        filter: true,
      },
      {
        name: 'Nested',
        children: [
          { name: 'Nested Leaf', prop: 'nested', size: 160, filter: true },
        ],
      },
    ],
  },
];

export const multiRowHeaderRows: DataType[] = [
  {
    athlete: 'Ada Stone',
    country: 'USA',
    sport: 'Cycling',
    total: 12,
    gold: 5,
    silver: 4,
    bronze: 3,
    rank: 1,
    score: 982,
    flat: 'Balanced A',
    nested: 'Nested A',
  },
  {
    athlete: 'Noah Smith',
    country: 'Canada',
    sport: 'Rowing',
    total: 9,
    gold: 3,
    silver: 4,
    bronze: 2,
    rank: 2,
    score: 913,
    flat: 'Balanced B',
    nested: 'Nested B',
  },
  {
    athlete: 'Mila Hart',
    country: 'France',
    sport: 'Fencing',
    total: 8,
    gold: 2,
    silver: 3,
    bronze: 3,
    rank: 3,
    score: 884,
    flat: 'Balanced C',
    nested: 'Nested C',
  },
  {
    athlete: 'Leo King',
    country: 'Japan',
    sport: 'Judo',
    total: 7,
    gold: 2,
    silver: 2,
    bronze: 3,
    rank: 4,
    score: 851,
    flat: 'Balanced D',
    nested: 'Nested D',
  },
];
Reacttsx
import { useMemo } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import {
  AdvanceFilterPlugin,
  ColumnCollapsePlugin,
  ColumnStretchPlugin,
  MultiRowHeaderPlugin,
  RowOddPlugin,
} from '@revolist/revogrid-pro';
import {
  multiRowHeaderColumns,
  multiRowHeaderRows,
} from './multi-row-header.shared';
import { currentTheme } from '../composables/useRandomData';
import './MultiRowHeader.scss';

const { isDark } = currentTheme();

export default function MultiRowHeader() {
  const darkTheme = isDark();
  const plugins = useMemo(
    () => [
      MultiRowHeaderPlugin,
      ColumnCollapsePlugin,
      AdvanceFilterPlugin,
      RowOddPlugin,
      ColumnStretchPlugin,
    ],
    [],
  );
  const columns = useMemo(() => multiRowHeaderColumns, []);
  const rows = useMemo(() => multiRowHeaderRows, []);

  return (
    <div className={`multi-row-header-example flex grow flex-col${darkTheme ? ' multi-row-header-example--dark' : ''}`}>
      <RevoGrid
        className="grow"
        columns={columns}
        source={rows}
        plugins={plugins}
        multiRowHeader={true}
        filter={true}
        stretch="all"
        theme={darkTheme ? 'darkMaterial' : 'material'}
        hide-attribution
      />
    </div>
  );
}
Vuevue
<template>
  <div
    class="multi-row-header-example grow flex flex-col"
    :class="{ 'multi-row-header-example--dark': isDark }"
  >
    <RevoGrid
      class="grow"
      :columns="columns"
      :source="rows"
      :plugins="plugins"
      :multi-row-header.prop="true"
      :filter="true"
      stretch="all"
      :theme="isDark ? 'darkMaterial' : 'material'"
      hide-attribution
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import {
  AdvanceFilterPlugin,
  ColumnCollapsePlugin,
  ColumnStretchPlugin,
  MultiRowHeaderPlugin,
  RowOddPlugin,
} from '@revolist/revogrid-pro';
import {
  multiRowHeaderColumns,
  multiRowHeaderRows,
} from './multi-row-header.shared';
import { currentThemeVue } from '../composables/useRandomData';
import './MultiRowHeader.scss';

const { isDark } = currentThemeVue();
const columns = ref(multiRowHeaderColumns);
const rows = ref(multiRowHeaderRows);
const plugins = [
  MultiRowHeaderPlugin,
  ColumnCollapsePlugin,
  AdvanceFilterPlugin,
  RowOddPlugin,
  ColumnStretchPlugin,
];
</script>
Angularts
import {
  Component,
  NO_ERRORS_SCHEMA,
  ViewEncapsulation,
} from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import {
  AdvanceFilterPlugin,
  ColumnCollapsePlugin,
  ColumnStretchPlugin,
  MultiRowHeaderPlugin,
  RowOddPlugin,
} from '@revolist/revogrid-pro';
import {
  multiRowHeaderColumns,
  multiRowHeaderRows,
} from './multi-row-header.shared';
import { currentTheme } from '../composables/useRandomData';

@Component({
  selector: 'multi-row-header-grid',
  standalone: true,
  imports: [RevoGrid],
  template: `
    <div
      class="multi-row-header-example"
      [class.multi-row-header-example--dark]="theme === 'darkMaterial'"
    >
      <revo-grid
        [columns]="columns"
        [source]="source"
        [plugins]="plugins"
        [multiRowHeader]="true"
        [filter]="true"
        stretch="all"
        [theme]="theme"
        [hideAttribution]="true"
      ></revo-grid>
    </div>
  `,
  styleUrls: ['./MultiRowHeader.scss'],
  encapsulation: ViewEncapsulation.None,
  schemas: [NO_ERRORS_SCHEMA],
})
export class MultiRowHeaderGridComponent {
  theme = currentTheme().isDark() ? 'darkMaterial' : 'material';
  columns = multiRowHeaderColumns;
  source = multiRowHeaderRows;
  plugins = [
    MultiRowHeaderPlugin,
    ColumnCollapsePlugin,
    AdvanceFilterPlugin,
    RowOddPlugin,
    ColumnStretchPlugin,
  ];
}
import { MultiRowHeaderPlugin } from '@revolist/revogrid-pro';
const grid = document.createElement('revo-grid');
grid.plugins = [MultiRowHeaderPlugin];
grid.multiRowHeader = true;

Define headers with the existing children column-group structure. Leaf columns still own data binding, sorting, filtering, editing, templates, and resizing.

grid.columns = [
{
name: 'Group A',
children: [
{ name: 'Athlete 1', prop: 'athlete' },
{
name: 'Group B',
children: [
{ name: 'Country 1', prop: 'country' },
{
name: 'Group C',
children: [
{ name: 'Sport 1', prop: 'sport' },
],
},
],
},
],
},
];

By default, shallow leaf headers span unused group rows. This matches the common spreadsheet behavior where a leaf header under a shallower branch fills the remaining vertical header space.

Disable spanning for a single column when you want balanced empty group space above the leaf:

grid.columns = [
{
name: 'Balanced',
children: [
{
name: 'Flat Leaf',
prop: 'flat',
suppressSpanHeaderHeight: true,
},
{
name: 'Nested',
children: [{ name: 'Nested Leaf', prop: 'nested' }],
},
],
},
];

You can also disable spanning for the whole plugin:

grid.multiRowHeader = {
spanLeafHeaders: false,
};

Set spanHeaderHeight on a column group when that group should fill every remaining grouped-header row plus the leaf-header row:

grid.columns = [
{
name: 'January',
spanHeaderHeight: true,
children: [
{ name: 'Period Total', prop: 'januaryTotal' },
],
},
{
name: 'February',
children: [
{ name: 'Sales', prop: 'februarySales' },
{ name: 'Profit', prop: 'februaryProfit' },
],
},
];

The descendant leaf remains the data column, but its header is covered by the spanning group. Explicit group spans are independent of spanLeafHeaders.

Pivot uses this option for generated collapsed column groups. With MultiRowHeaderPlugin enabled, the collapsed group label fills the remaining header depth. Without it, the regular collapsed placeholder header remains visible as a fallback.

When Pivot row-axis headers contain sorting, selection counts, or filter controls, keep those leaf headers at their normal height while retaining collapsed group spanning:

grid.multiRowHeader = {
spanLeafHeaders: false,
};

spanHeaderHeight remains active because explicit group spanning is independent of spanLeafHeaders.

grid.multiRowHeader = {
spanLeafHeaders: true,
};
OptionDefaultDescription
spanLeafHeaderstrueLets shallow leaf headers fill unused group depth.
  • Works with nested ColumnGrouping definitions.
  • Leaf headers keep sorting, filtering, custom templates, and resize behavior.
  • Group headers continue through RevoGrid’s normal group header render hooks.
  • ColumnCollapsePlugin can be registered with MultiRowHeaderPlugin to add expand/collapse controls to groups.
  • spanHeaderHeight lets a terminal group fill the remaining header depth while retaining its descendant data column.
  • Pinned columns can split a group into pinned and unpinned visible bands, matching the behavior used by major data grids.