Skip to content

Row Pinning

Enterprise

Row pinning holds chosen rows in a fixed band above or below the scrolling body, so they stay in view no matter where you scroll. Two things can be pinned:

  • Existing dataset rows, pinned by a stable identity — the pin follows the row through sorting, filtering and pagination rather than sticking to a screen position.
  • Standalone rows supplied by data — a header, a caption, or a computed grand-total row that is not part of the dataset. This is the equivalent of AG Grid’s pinnedTopRowData / pinnedBottomRowData.

Both render in the same top/bottom bands, with the standalone rows sitting outermost: data rows above the pinned dataset rows on top, and below them on the bottom.

Row pinning ships in @zengrid/enterprise. A RowPinManager is the headless source of truth for what’s pinned; you hand it to the grid through the pinProvider option via rowPinning():

import { RowPinManager, rowPinning } from '@zengrid/enterprise/grid';
const pins = new RowPinManager();
pins.pin(0, 'top'); // pin a dataset row (by id) to the top band
pins.pin(42, 'bottom'); // …and one to the bottom
const grid = new Zengrid(el, {
rowCount,
pinProvider: rowPinning(pins), // Enterprise: turn on row pinning
columns: [/* … */],
});

Pins are keyed by a stable row id, not a render index, so they survive sort, filter and pagination — the provider resolves each pinned id back to the row it currently occupies every render pass, and re-renders the bands automatically whenever you pin, unpin, or replace the standalone rows.

To pin rows that aren’t in the dataset — a header or a computed total — give the manager topRowData / bottomRowData, or set them later with setPinnedData. Each is a plain object whose cells resolve by the column’s field:

const pins = new RowPinManager({
bottomRowData: [{ data: { name: 'Grand total', revenue: 1_240_000 } }],
});
// …later, recompute and swap the band in place:
pins.setPinnedData('bottom', [{ data: { name: 'Grand total', revenue: next } }]);
type RowId = string | number;
type RowPinPosition = 'top' | 'bottom';
interface RowPinManagerConfig {
/** Directions manual pinning may target. true = both. @default true */
enabled?: boolean | RowPinPosition;
/** Guard consulted before a row is pinned — return false to forbid it. */
isRowPinnable?: (id: RowId) => boolean;
/** Seed initial pins: called for each id in `rows`. */
isRowPinned?: (id: RowId) => RowPinPosition | null | undefined;
rows?: readonly RowId[];
/** Standalone (non-dataset) rows pinned to each band. */
topRowData?: readonly PinnedDataRow[];
bottomRowData?: readonly PinnedDataRow[];
}
interface PinnedDataRow {
/** Row object; each cell reads `data[column.field]`. */
data: unknown;
/** Explicit row height in px; falls back to the grid's default. */
height?: number;
}

Key manager methods: pin(id, position), unpin(id), toggle(id, position), pinnedAt(id), getPinned(position), setPinnedData(position, rows), clear(), plus onChange (dataset-row pins) and onDataChange (standalone rows).

Each tab pins a different way. Edit the snippet — change which rows are pinned, flip a band, or recompute a total — and the grid re-renders its bands.

pins.pin(id, band) fixes rows by id. Scroll the body — the first and last rows stay in their bands. Add pins.pin(2, "top") to pin another.

  • Pins are identity-based. pins.pin(3, 'top') pins the row whose id is 3, not the fourth row on screen. When you sort or filter, the provider re-resolves each pinned id to its new position, so the pin follows the row.
  • Pinned rows also remain in the body. The bands are opaque overlays fixed to the top and bottom of the scroll area — a pinned row is drawn in its band and scrolls past underneath. This matches the familiar pinned-row behaviour.
  • Standalone rows aren’t dataset rows. They ignore the enable mode and the isRowPinnable guard, are unaffected by sort/filter, and resolve each cell from their own object by the column’s field. Give one an explicit height to make a taller summary row.
  • Bands re-render on change. Pinning, unpinning, or setPinnedData all notify the grid — no manual refresh needed.