Sticky Cells
Sticky Cells keep important checkpoint cells visible while users scroll through a virtualized grid. Mark cells with cellProperties; when the row crosses the top visibility edge, the marked cells render in an extra header row until the next sticky row replaces them.
Source code
import { defineCustomElements } from '@revolist/revogrid/loader';import type { CellTemplateProp, ColumnGrouping, ColumnRegular } from '@revolist/revogrid';import { AdvanceFilterPlugin, ColumnStretchPlugin, FilterHeaderPlugin, StickyCellsPlugin,} from '@revolist/revogrid-pro';import { currentTheme } from '../composables/useRandomData';
defineCustomElements();
const { isDark } = currentTheme();
const stickyCellProperties = (props: CellTemplateProp) => { if (props.rowIndex % 10 !== 0) { return undefined; }
return { 'sticky-cell': true, class: { 'sticky-source-cell': true, }, };};
const columns: (ColumnRegular | ColumnGrouping)[] = [ { name: 'Pinned Identity', children: [ { name: 'Account', prop: 'account', pin: 'colPinStart', size: 150, filter: true, cellProperties: stickyCellProperties, }, ], }, { name: 'Opportunity Workflow', children: [ { name: 'Stage', prop: 'stage', size: 130, filter: ['selection'], cellProperties: stickyCellProperties, }, { name: 'Status', prop: 'status', size: 140, filter: ['selection'], cellProperties: stickyCellProperties, cellTemplate: (h, props) => h('span', { class: 'sticky-status' }, props.value), }, { name: 'Owner', prop: 'owner', size: 130, filter: ['selection'], }, { name: 'Amount', prop: 'amount', size: 120, filter: true, }, ], }, { name: 'Pinned Market', children: [ { name: 'Pinned End', prop: 'region', pin: 'colPinEnd', size: 130, filter: ['selection'], cellProperties: stickyCellProperties, }, ], },];
const rows = Array.from({ length: 120 }, (_, index) => ({ account: `Account ${index}`, stage: index % 10 === 0 ? `Milestone ${index / 10 + 1}` : `Stage ${(index % 4) + 1}`, status: index % 10 === 0 ? 'Sticky checkpoint' : index % 3 === 0 ? 'Blocked' : 'In progress', owner: ['Ada', 'Grace', 'Linus', 'Margaret'][index % 4], amount: `$${(1250 + index * 175).toLocaleString()}`, region: ['North', 'South', 'West'][index % 3],}));
export function load(parentSelector: string) { const parent = document.querySelector(parentSelector); if (!parent) { return; }
const grid = document.createElement('revo-grid'); grid.className = 'sticky-cells-demo'; grid.style.height = '420px'; grid.theme = isDark() ? 'darkMaterial' : 'material'; grid.columns = columns; grid.plugins = [AdvanceFilterPlugin, FilterHeaderPlugin, StickyCellsPlugin, ColumnStretchPlugin]; grid.additionalData = { stretch: 'all', }; grid.filter = {}; grid.hideAttribution = true;
parent.appendChild(grid); grid.source = rows;
return () => grid.remove();}<template> <VGrid class="sticky-cells-demo" :theme="isDark ? 'darkMaterial' : 'material'" :columns="columns" :source="rows" :plugins="plugins" :additional-data="additionalData" :filter="filter" hide-attribution /></template>
<script setup lang="ts">import { computed, ref } from 'vue';import { VGrid } from '@revolist/vue3-datagrid';import type { CellTemplateProp, ColumnGrouping, ColumnRegular } from '@revolist/revogrid';import { AdvanceFilterPlugin, ColumnStretchPlugin, FilterHeaderPlugin, StickyCellsPlugin,} from '@revolist/revogrid-pro';import { currentThemeVue } from '../composables/useRandomData';
const { isDark } = currentThemeVue();
const plugins = [AdvanceFilterPlugin, FilterHeaderPlugin, StickyCellsPlugin, ColumnStretchPlugin];const filter = ref({});
const stickyCellProperties = (props: CellTemplateProp) => { if (props.rowIndex % 10 !== 0) { return undefined; }
return { 'sticky-cell': true, class: { 'sticky-source-cell': true, }, };};
const columns = ref<(ColumnRegular | ColumnGrouping)[]>([ { name: 'Pinned Identity', children: [ { name: 'Account', prop: 'account', pin: 'colPinStart', size: 150, filter: true, cellProperties: stickyCellProperties, }, ], }, { name: 'Opportunity Workflow', children: [ { name: 'Stage', prop: 'stage', size: 130, filter: ['selection'], cellProperties: stickyCellProperties, }, { name: 'Status', prop: 'status', size: 140, filter: ['selection'], cellProperties: stickyCellProperties, cellTemplate: (h, props) => h('span', { class: 'sticky-status' }, props.value), }, { name: 'Owner', prop: 'owner', filter: ['selection'], }, { name: 'Amount', prop: 'amount', filter: true, }, ], }, { name: 'Pinned Market', children: [ { name: 'Pinned End', prop: 'region', pin: 'colPinEnd', size: 130, filter: ['selection'], cellProperties: stickyCellProperties, }, ], },]);
const rows = ref( Array.from({ length: 120 }, (_, index) => ({ account: `Account ${index}`, stage: index % 10 === 0 ? `Milestone ${index / 10 + 1}` : `Stage ${(index % 4) + 1}`, status: index % 10 === 0 ? 'Sticky checkpoint' : index % 3 === 0 ? 'Blocked' : 'In progress', owner: ['Ada', 'Grace', 'Linus', 'Margaret'][index % 4], amount: `$${(1250 + index * 175).toLocaleString()}`, region: ['North', 'South', 'West'][index % 3], })),);
const additionalData = computed(() => ({ stretch: 'all',}));</script>
<style scoped>.sticky-cells-demo { display: block; height: 420px;}
:global(.sticky-cells-demo .sticky-status) { display: inline-flex; align-items: center; height: 100%; font-weight: 600;}
:global(.sticky-cells-demo .rgCell[sticky-cell-overlay]) { font-weight: 600;}</style>import React, { useMemo } from 'react';import { RevoGrid } from '@revolist/react-datagrid';import type { CellTemplateProp, ColumnGrouping, ColumnRegular } from '@revolist/revogrid';import { AdvanceFilterPlugin, ColumnStretchPlugin, FilterHeaderPlugin, StickyCellsPlugin,} from '@revolist/revogrid-pro';import { currentTheme } from '../composables/useRandomData';
const { isDark } = currentTheme();
function createRows() { return Array.from({ length: 120 }, (_, index) => ({ account: `Account ${index}`, stage: index % 10 === 0 ? `Milestone ${index / 10 + 1}` : `Stage ${(index % 4) + 1}`, status: index % 10 === 0 ? 'Sticky checkpoint' : index % 3 === 0 ? 'Blocked' : 'In progress', owner: ['Ada', 'Grace', 'Linus', 'Margaret'][index % 4], amount: `$${(1250 + index * 175).toLocaleString()}`, region: ['North', 'South', 'West'][index % 3], }));}
export default function StickyCells() { const plugins = useMemo( () => [AdvanceFilterPlugin, FilterHeaderPlugin, StickyCellsPlugin, ColumnStretchPlugin], [], ); const additionalData = useMemo(() => ({ stretch: 'all' }), []); const filter = useMemo(() => ({}), []); const rows = useMemo(createRows, []);
const columns = useMemo<(ColumnRegular | ColumnGrouping)[]>(() => { const stickyCellProperties = (props: CellTemplateProp) => { if (props.rowIndex % 10 !== 0) { return undefined; }
return { 'sticky-cell': true, class: { 'sticky-source-cell': true, }, }; };
return [ { name: 'Pinned Identity', children: [ { name: 'Account', prop: 'account', pin: 'colPinStart', size: 150, filter: true, cellProperties: stickyCellProperties, }, ], }, { name: 'Opportunity Workflow', children: [ { name: 'Stage', prop: 'stage', size: 130, filter: ['selection'], cellProperties: stickyCellProperties, }, { name: 'Status', prop: 'status', size: 140, filter: ['selection'], cellProperties: stickyCellProperties, cellTemplate: (h, props) => h('span', { class: 'sticky-status' }, props.value), }, { name: 'Owner', prop: 'owner', size: 130, filter: ['selection'], }, { name: 'Amount', prop: 'amount', size: 120, filter: true, }, ], }, { name: 'Pinned Market', children: [ { name: 'Pinned End', prop: 'region', pin: 'colPinEnd', size: 130, filter: ['selection'], cellProperties: stickyCellProperties, }, ], }, ]; }, []);
return ( <RevoGrid className="sticky-cells-demo" theme={isDark() ? 'darkMaterial' : 'material'} columns={columns} source={rows} plugins={plugins} additionalData={additionalData} filter={filter} hideAttribution style={{ height: 420 }} /> );}import { CommonModule } from '@angular/common';import { Component, ViewEncapsulation } from '@angular/core';import { RevoGrid } from '@revolist/angular-datagrid';import type { CellTemplateProp, ColumnGrouping, ColumnRegular } from '@revolist/revogrid';import { AdvanceFilterPlugin, ColumnStretchPlugin, FilterHeaderPlugin, StickyCellsPlugin,} from '@revolist/revogrid-pro';import { currentTheme } from '../composables/useRandomData';
const { isDark } = currentTheme();
const stickyCellProperties = (props: CellTemplateProp) => { if (props.rowIndex % 10 !== 0) { return undefined; }
return { 'sticky-cell': true, class: { 'sticky-source-cell': true, }, };};
@Component({ selector: 'sticky-cells-grid', standalone: true, imports: [CommonModule, RevoGrid], encapsulation: ViewEncapsulation.None, template: ` <revo-grid class="sticky-cells-demo" [theme]="theme" [columns]="columns" [source]="rows" [plugins]="plugins" [additionalData]="additionalData" [filter]="filter" [hideAttribution]="true" style="height: 420px;" ></revo-grid> `,})export class StickyCellsGridComponent { readonly theme = isDark() ? 'darkMaterial' : 'material'; readonly plugins = [AdvanceFilterPlugin, FilterHeaderPlugin, StickyCellsPlugin, ColumnStretchPlugin]; readonly additionalData = { stretch: 'all', }; readonly filter = {};
readonly columns: (ColumnRegular | ColumnGrouping)[] = [ { name: 'Pinned Identity', children: [ { name: 'Account', prop: 'account', pin: 'colPinStart', size: 150, filter: true, cellProperties: stickyCellProperties, }, ], }, { name: 'Opportunity Workflow', children: [ { name: 'Stage', prop: 'stage', size: 130, filter: ['selection'], cellProperties: stickyCellProperties, }, { name: 'Status', prop: 'status', size: 140, filter: ['selection'], cellProperties: stickyCellProperties, cellTemplate: (h, props) => h('span', { class: 'sticky-status' }, props.value), }, { name: 'Owner', prop: 'owner', size: 130, filter: ['selection'], }, { name: 'Amount', prop: 'amount', size: 120, filter: true, }, ], }, { name: 'Pinned Market', children: [ { name: 'Pinned End', prop: 'region', pin: 'colPinEnd', size: 130, filter: ['selection'], cellProperties: stickyCellProperties, }, ], }, ];
readonly rows = Array.from({ length: 120 }, (_, index) => ({ account: `Account ${index}`, stage: index % 10 === 0 ? `Milestone ${index / 10 + 1}` : `Stage ${(index % 4) + 1}`, status: index % 10 === 0 ? 'Sticky checkpoint' : index % 3 === 0 ? 'Blocked' : 'In progress', owner: ['Ada', 'Grace', 'Linus', 'Margaret'][index % 4], amount: `$${(1250 + index * 175).toLocaleString()}`, region: ['North', 'South', 'West'][index % 3], }));}Add StickyCellsPlugin to the grid and return { 'sticky-cell': true } from a column cellProperties callback.
import { StickyCellsPlugin } from '@revolist/revogrid-pro';
const stickyCellProperties = (props) => { return props.rowIndex % 10 === 0 ? { 'sticky-cell': true } : undefined;};
const columns = [ { name: 'Account', prop: 'account', cellProperties: stickyCellProperties, }, { name: 'Status', prop: 'status', cellProperties: stickyCellProperties, },];
grid.plugins = [StickyCellsPlugin];If any cell in a row is marked as sticky, that row can become the active sticky row. The sticky header renders only the cells whose own cellProperties returned 'sticky-cell': true.
With Other Header Plugins
Section titled “With Other Header Plugins”Sticky Cells render through the header layer, so the plugin can be combined with grouped headers and filter headers. When using the filter header plugin, register filtering before sticky cells so Sticky Cells wraps the final header content:
import { AdvanceFilterPlugin, FilterHeaderPlugin, StickyCellsPlugin, ColumnStretchPlugin,} from '@revolist/revogrid-pro';
grid.plugins = [ AdvanceFilterPlugin, FilterHeaderPlugin, StickyCellsPlugin, ColumnStretchPlugin,];
grid.filter = {};grid.additionalData = { stretch: 'all',};Behavior
Section titled “Behavior”- One sticky row is active at a time.
- A marked row becomes sticky only after the bottom edge of that source row scrolls above the viewport top.
- Sticky cells work across
colPinStart,rgCol, andcolPinEnd. - Custom
cellTemplateoutput is reused in the sticky header row. - The original marked cells are hidden only while their sticky overlay is active.