Sticky Cells
Sticky Cells keep important checkpoint cells visible while users scroll through a virtualized grid. Mark cells with stickyCell; when the row starts crossing the top visibility edge, the marked row renders through the pinned top row area and is trimmed from the main viewport until the next sticky row replaces it.
Source code
TypeScript ts
import { defineCustomElements } from '@revolist/revogrid/loader';
import type { CellTemplateProp, ColumnGrouping, ColumnRegular } from '@revolist/revogrid';
import {
AdvanceFilterPlugin,
ColumnStretchPlugin,
FilterHeaderPlugin,
RowOddPlugin,
StickyCellsPlugin,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
defineCustomElements();
const { isDark } = currentTheme();
const stickyCell = (props: CellTemplateProp) => props.model.status === 'Sticky checkpoint';
const stickySourceCellProperties = (props: CellTemplateProp) => stickyCell(props)
? {
class: {
'sticky-source-cell': true,
},
}
: undefined;
const columns: (ColumnRegular | ColumnGrouping)[] = [
{
name: 'Pinned Identity',
children: [
{
name: 'Account',
prop: 'account',
pin: 'colPinStart',
size: 150,
filter: true,
stickyCell,
cellProperties: stickySourceCellProperties,
},
],
},
{
name: 'Opportunity Workflow',
children: [
{
name: 'Stage',
prop: 'stage',
size: 130,
filter: ['selection'],
stickyCell,
cellProperties: stickySourceCellProperties,
},
{
name: 'Status',
prop: 'status',
size: 140,
filter: ['selection'],
stickyCell,
cellProperties: stickySourceCellProperties,
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'],
stickyCell,
cellProperties: stickySourceCellProperties,
},
],
},
];
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, RowOddPlugin, StickyCellsPlugin, ColumnStretchPlugin];
grid.stretch = 'last';
grid.filter = {};
grid.hideAttribution = true;
parent.appendChild(grid);
grid.source = rows;
return () => grid.remove();
}
Vue vue
<template>
<VGrid
class="sticky-cells-demo grow"
:theme="isDark ? 'darkMaterial' : 'material'"
:columns="columns"
:source="rows"
:plugins="plugins"
stretch="all"
:filter="filter"
hide-attribution
/>
</template>
<script setup lang="ts">
import { ref, shallowRef } from 'vue';
import { VGrid } from '@revolist/vue3-datagrid';
import type { CellTemplateProp, ColumnGrouping, ColumnRegular } from '@revolist/revogrid';
import {
AdvanceFilterPlugin,
ColumnStretchPlugin,
FilterHeaderPlugin,
RowOddPlugin,
StickyCellsPlugin,
} from '@revolist/revogrid-pro';
import { currentThemeVue } from '../composables/useRandomData';
const { isDark } = currentThemeVue();
const plugins = [AdvanceFilterPlugin, FilterHeaderPlugin, RowOddPlugin, StickyCellsPlugin, ColumnStretchPlugin];
const filter = ref({});
const stickyCell = (props: CellTemplateProp) => props.model.status === 'Sticky checkpoint';
const stickySourceCellProperties = (props: CellTemplateProp) => stickyCell(props)
? {
class: {
'sticky-source-cell': true,
},
}
: undefined;
const columns = shallowRef<(ColumnRegular | ColumnGrouping)[]>([
{
name: 'Pinned Identity',
children: [
{
name: 'Account',
prop: 'account',
pin: 'colPinStart',
size: 150,
filter: true,
sortable: true,
stickyCell,
cellProperties: stickySourceCellProperties,
},
],
},
{
name: 'Opportunity Workflow',
children: [
{
name: 'Stage',
prop: 'stage',
size: 130,
filter: ['selection'],
stickyCell,
cellProperties: stickySourceCellProperties,
},
{
name: 'Status',
prop: 'status',
size: 140,
filter: ['selection'],
stickyCell,
cellProperties: stickySourceCellProperties,
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'],
stickyCell,
cellProperties: stickySourceCellProperties,
},
],
},
]);
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],
})),
);
</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;
}
</style>
React tsx
import React, { useMemo } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import type { CellTemplateProp, ColumnGrouping, ColumnRegular } from '@revolist/revogrid';
import {
AdvanceFilterPlugin,
ColumnStretchPlugin,
FilterHeaderPlugin,
RowOddPlugin,
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, RowOddPlugin, StickyCellsPlugin, ColumnStretchPlugin],
[],
);
const filter = useMemo(() => ({}), []);
const rows = useMemo(createRows, []);
const columns = useMemo<(ColumnRegular | ColumnGrouping)[]>(() => {
const stickyCell = (props: CellTemplateProp) => props.model.status === 'Sticky checkpoint';
const stickySourceCellProperties = (props: CellTemplateProp) => stickyCell(props)
? {
class: {
'sticky-source-cell': true,
},
}
: undefined;
return [
{
name: 'Pinned Identity',
children: [
{
name: 'Account',
prop: 'account',
pin: 'colPinStart',
size: 150,
filter: true,
stickyCell,
cellProperties: stickySourceCellProperties,
},
],
},
{
name: 'Opportunity Workflow',
children: [
{
name: 'Stage',
prop: 'stage',
size: 130,
filter: ['selection'],
stickyCell,
cellProperties: stickySourceCellProperties,
},
{
name: 'Status',
prop: 'status',
size: 140,
filter: ['selection'],
stickyCell,
cellProperties: stickySourceCellProperties,
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'],
stickyCell,
cellProperties: stickySourceCellProperties,
},
],
},
];
}, []);
return (
<RevoGrid
className="sticky-cells-demo grow"
theme={isDark() ? 'darkMaterial' : 'material'}
columns={columns}
source={rows}
plugins={plugins}
stretch="all"
filter={filter}
hideAttribution
style={{ height: 420 }}
/>
);
}
Angular ts
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,
RowOddPlugin,
StickyCellsPlugin,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
const { isDark } = currentTheme();
const stickyCell = (props: CellTemplateProp) => props.model.status === 'Sticky checkpoint';
const stickySourceCellProperties = (props: CellTemplateProp) => stickyCell(props)
? {
class: {
'sticky-source-cell': true,
},
}
: undefined;
@Component({
selector: 'sticky-cells-grid',
standalone: true,
imports: [CommonModule, RevoGrid],
encapsulation: ViewEncapsulation.None,
template: `
<revo-grid
class="sticky-cells-demo grow"
[theme]="theme"
[columns]="columns"
[source]="rows"
[plugins]="plugins"
[stretch]="additionalData.stretch"
[filter]="filter"
[hideAttribution]="true"
style="min-height: 420px;"
></revo-grid>
`,
})
export class StickyCellsGridComponent {
readonly theme = isDark() ? 'darkMaterial' : 'material';
readonly plugins = [AdvanceFilterPlugin, FilterHeaderPlugin, RowOddPlugin, 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,
stickyCell,
cellProperties: stickySourceCellProperties,
},
],
},
{
name: 'Opportunity Workflow',
children: [
{
name: 'Stage',
prop: 'stage',
size: 130,
filter: ['selection'],
stickyCell,
cellProperties: stickySourceCellProperties,
},
{
name: 'Status',
prop: 'status',
size: 140,
filter: ['selection'],
stickyCell,
cellProperties: stickySourceCellProperties,
cellTemplate: (h, props) => h('span', { class: 'sticky-status' }, props.value),
},
{
name: 'Owner',
prop: 'owner',
size: 130,
filter: ['selection'],
},
{
name: 'Amount',
prop: 'amount',
filter: true,
},
],
},
{
name: 'Pinned Market',
children: [
{
name: 'Pinned End',
prop: 'region',
pin: 'colPinEnd',
size: 130,
filter: ['selection'],
stickyCell,
cellProperties: stickySourceCellProperties,
},
],
},
];
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 define a column stickyCell predicate.
import { StickyCellsPlugin } from '@revolist/revogrid-pro';
const stickyCell = ({ model }) => model.isCheckpoint === true;
const columns = [ { name: 'Account', prop: 'account', stickyCell, }, { name: 'Status', prop: 'status', stickyCell, },];
grid.plugins = [StickyCellsPlugin];If any cell in a row is marked as sticky, that row can become the active sticky row.
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 by default. Set
stickyCells.maxRowsto keep multiple recent sticky rows pinned. - A marked row becomes sticky once 10% of that source row has scrolled above the viewport top.
- Sticky cells work across
colPinStart,rgCol, andcolPinEnd. - Custom
cellTemplateoutput is reused in the pinned sticky row. - Existing
pinnedTopSourcerows remain before plugin-managed sticky rows. - Active sticky source rows are trimmed out of the main row viewport.