Skip to content

Sticky Rows Plugin

The Sticky Rows plugin keeps selected data rows pinned just under the header while the user scrolls past them. Useful for section markers, group headers in flat lists, “you are here” anchors, and any scenario where you want a row to remain visible after it would naturally scroll out of view.

Stuck rows are clones of the real rows — the originals stay in the data flow, remain interactive, and continue to participate in keyboard navigation. Clones are decorative, marked aria-hidden="true", and inherit the row’s column-template alignment so they line up perfectly with the data below.

Scroll the grid below. Section-marker rows (— A —, — B —, …) pin under the header as they scroll past. Switch between push and stack mode in the controls to compare behaviors, and increase Rows per section to see longer scroll runs between markers.

Mode 'push' keeps one stuck row at a time (iOS section-header behavior); 'stack' accumulates them below the header.
Max stacked Cap on stacked clones (only meaningful when mode = 'stack').
Section count How many letter-group sections to generate.
Rows per section Rows in each section.
import '@toolbox-web/grid/features/sticky-rows';

Or use the plugin directly:

import { StickyRowsPlugin } from '@toolbox-web/grid/plugins/sticky-rows';

Provide an isSticky predicate — either the name of a boolean field on your row data, or a function receiving (row, index) and returning a truthy value when the row should be sticky.

import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/sticky-rows';
const grid = queryGrid('tbw-grid');
grid.gridConfig = {
columns: [
{ field: 'label', header: 'Label' },
{ field: 'value', header: 'Value' },
],
features: {
// Field-name shorthand: any row whose `isSection` is truthy is sticky.
stickyRows: { isSticky: 'isSection' },
},
};

Only one sticky row is shown at a time. As the next sticky row approaches from below, it slides the previous one upward and out of view (iOS section-header behavior).

features: { stickyRows: { isSticky: 'isSection', mode: 'push' } }

Best for: long flat lists where each section anchor should be transient.

Sticky rows accumulate below the header up to maxStacked. When the cap is reached, the oldest (lowest-index) entry is evicted from the top so the most recently passed marker is always visible.

features: {
stickyRows: {
isSticky: 'isSection',
mode: 'stack',
maxStacked: 3,
},
}

Best for: hierarchical breadcrumbs where multiple levels of context should remain visible.

For more nuanced selection (computed flags, derived fields, every Nth row, etc.), pass a function:

features: {
stickyRows: {
isSticky: (row, index) => row.priority === 'critical' || index % 50 === 0,
},
}

The predicate runs once per row whenever the row set changes (via afterRender), so keep it cheap.

OptionTypeDefaultDescription
isStickystring | (row, index) => unknownrequiredField name shorthand or predicate function.
mode'push' | 'stack''push'How concurrent sticky rows are presented.
maxStackednumberInfinityCap on stacked clones (only applies when mode: 'stack').
classNamestring(none)Optional class applied to the container and every clone.

The plugin renders a single <div class="tbw-sticky-rows"> between the header and the rows region. Each clone carries class="tbw-sticky-row" and the data attribute data-sticky-row="<rowIndex>".

The container reads three CSS custom properties from the active theme:

  • --tbw-z-layer-sticky-rows — z-index (defaults to 22)
  • --tbw-color-bg / --tbw-color-panel-bg — background fill
  • --tbw-color-border — bottom-shadow color separating clones from data

Override with your own scoped rules:

tbw-grid .tbw-sticky-row {
background: var(--my-section-bg);
font-weight: 600;
}
  • Clones are marked aria-hidden="true" so screen readers don’t double-read.
  • Focus styles and tabindex are stripped from clones — keyboard navigation goes through the underlying rows only.
  • The originals retain their aria-rowindex, role="row", and full cell semantics, so screen readers describe row position relative to the dataset.

The plugin is ≈4 kB gzipped (ESM) / ≈2 kB gzipped (UMD). It does not pull in any other plugins or core internals.

AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://toolboxjs.com/llms-full.txt