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
TypeScript ts
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 columns ts
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',
  },
];
React tsx
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>
  );
}
Vue vue
<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>
Angular ts
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,
};
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.
  • Pinned columns can split a group into pinned and unpinned visible bands, matching the behavior used by major data grids.