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.

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' },
},
};

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 stackedCap on stacked clones (only meaningful when mode = 'stack').
Section countHow many letter-group sections to generate.
Rows per sectionRows in each section.

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.

See StickyRowsConfig for the full list of options and defaults. isSticky is the only required option — pass a field name as shorthand, or a predicate for anything more involved.

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 satisfies WCAG 2.2 SC 2.4.11 Focus Not Obscured (Minimum).

The clone overlay is absolutely positioned over the top of the rows viewport, so a row scrolled to the exact top would land underneath it and its focus indicator would be completely hidden. The plugin reports the overlay’s height to the grid, which offsets keyboard scrolling by that much — arrowing upwards always brings the focused row clear of the stuck clones.

When the focused row is one of the stuck clones it is already pinned in view, so the grid is told to skip scrolling entirely: scrolling to it would only push the row it is stuck above out of reach.

This is automatic and needs no configuration.

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