Skip to content

Pinned Rows (Status Bar) Plugin

The Pinned Rows plugin creates a fixed status bar at the top or bottom of the grid for displaying aggregations, row counts, or custom content. Think of it as the “totals row” you’d see in a spreadsheet—always visible regardless of scroll position.

import '@toolbox-web/grid/features/pinned-rows';

Pinned rows are configured with a unified slots[] array. Each slot becomes its own DOM row at position: 'top' or position: 'bottom' (default 'bottom'), in declared order. A slot is either an aggregation row (sum/avg/min/max/count/first/last or a custom function per column) or a panel row (a render function — built-in or custom).

import { queryGrid } from '@toolbox-web/grid';
import { rowCountPanel } from '@toolbox-web/grid/plugins/pinned-rows';
const grid = queryGrid('tbw-grid');
grid.gridConfig = {
columns: [
{ field: 'product', header: 'Product' },
{ field: 'quantity', header: 'Qty', type: 'number' },
{ field: 'price', header: 'Price', type: 'currency' },
],
features: {
pinnedRows: {
slots: [
{
id: 'totals',
position: 'bottom',
aggregators: {
quantity: 'sum',
price: { aggFunc: 'sum', formatter: (v) => `$${v.toFixed(2)}` },
},
cells: { product: 'Totals:' },
},
{ id: 'count', position: 'bottom', render: rowCountPanel() },
],
},
},
};

Use the controls to explore aggregation rows (top or bottom, single or stacked, full-width or per-column), the built-in row-count status panel and a custom right-zone panel. Every behaviour below is driven by the same slots[] array.

Position Place the aggregation slot above or below the data
Multiple aggregation rows Show three rows: sum, average, and min/max
Full-width mode Render aggregation rows as a single spanning cell with inline label
Row count panel Built-in rowCountPanel() — always visible
Custom right-zone panel A custom panel slot rendered in the right zone
OptionTypeDefaultDescription
slotsPinnedRowSlot[][]Unified ordered list of pinned-row slots (see Slots API)
fullWidthbooleanfalseDefault fullWidth mode applied to every aggregation slot

Legacy fieldsposition, showRowCount, showSelectedCount, showFilteredCount, aggregationRows, customPanels — are still accepted for backwards compatibility but are deprecated and ignored when slots is set. They will be removed in a future major release. New code should use slots. See the migration notes below.

The slots[] array is a unified, ordered list of pinned-row entries. Each slot becomes its own DOM row inside its position area ('top' or 'bottom', default 'bottom'), in declared order. Two slot shapes coexist:

  • Aggregation slot — anything without a render field is treated as an aggregation row (same shape as AggregationRowConfig: aggregators, cells, label, fullWidth).
  • Panel slot — anything with a render field. render is either a (ctx) => HTMLElement | null function (rendered into the left zone), or an array of { zone?: 'left' | 'center' | 'right', render } entries.

A panel render that returns null skips that contribution; a slot whose renderers all return null is dropped from the DOM. This is how the built-in selectedCountPanel and filteredCountPanel self-hide when their count is zero / unfiltered.

import {
filteredCountPanel,
rowCountPanel,
selectedCountPanel,
} from '@toolbox-web/grid/plugins/pinned-rows';
RendererAlways renders?
rowCountPanel()Yes
selectedCountPanel()Only when selectedRows > 0
filteredCountPanel()Only when filteredRows !== totalRows
import {
filteredCountPanel,
rowCountPanel,
selectedCountPanel,
} from '@toolbox-web/grid/plugins/pinned-rows';
features: {
pinnedRows: {
slots: [
// Aggregation row at the top
{ position: 'top', aggregators: { price: 'sum' }, cells: { id: 'Totals:' } },
// Three stacked status-panel rows at the bottom (in this order)
{ position: 'bottom', render: rowCountPanel() },
{ position: 'bottom', render: filteredCountPanel() },
{ position: 'bottom', render: selectedCountPanel() },
// Mixed-zone panel: left + right content in one row
{
position: 'bottom',
render: [
{ zone: 'left', render: (ctx) => myLeftBadge(ctx) },
{ zone: 'right', render: (ctx) => myRightBadge(ctx) },
],
},
],
},
},

All three adapters bridge slots[].render automatically — return your framework’s native node and the adapter wraps it in a vanilla DOM element via the existing portal/teleport/component-rendering machinery:

Adapterrender may return
ReactReactNode | null (e.g. <span>...</span>)
VueVNode | null (e.g. h('span', ...))
AngularType<unknown> | null (a component class)

An aggregation slot (any slot without a render field) computes values from the current rows on a per-column basis:

features: {
pinnedRows: {
slots: [
{
id: 'totals',
position: 'bottom',
aggregators: {
// Simple string aggregator
quantity: 'sum',
// Object syntax with formatter
price: {
aggFunc: 'sum',
formatter: (value) => `$${value.toFixed(2)}`,
},
},
cells: { id: 'Totals:', name: '' },
},
],
},
},
SyntaxExampleDescription
String'sum'Built-in aggregator
Function(rows, field) => rows.lengthCustom aggregator function
Object{ aggFunc: 'sum', formatter: (v) => v.toFixed(2) }Aggregator with formatter

sum, avg, count, min, max, first, last

When fullWidth is true, an aggregation slot renders as a single spanning cell with the label and aggregated values displayed inline—similar to the row grouping plugin’s full-width mode. When false (the default), each column gets its own cell aligned to the grid template.

The label property works in both modes. In per-column mode it renders as an overlay at the left edge of the row, independent of column width—so the text won’t truncate even if the first column is narrow.

Set fullWidth globally on the plugin config or per-slot on the aggregation slot. Per-slot settings override the global default:

features: {
pinnedRows: {
fullWidth: true, // All aggregation slots span full width by default
slots: [
{
id: 'totals',
label: 'Totals',
aggregators: { quantity: 'sum', price: 'sum' },
},
{
id: 'detail',
fullWidth: false, // Override: render per-column
aggregators: { quantity: 'avg', price: 'avg' },
cells: { product: 'Averages:' },
},
],
},
},

Use a panel slot — any slot with a render field — to inject custom content. render is either a single function (rendered into the left zone) or an array of { zone, render } entries to populate 'left', 'center', and/or 'right' zones in one row:

features: {
pinnedRows: {
slots: [
{
id: 'info',
position: 'bottom',
render: [
{
zone: 'right',
render: (ctx) => {
const el = document.createElement('span');
el.textContent = `Rows: ${ctx.totalRows}`;
return el;
},
},
],
},
],
},
},

Return null from a render function to skip that contribution; if every contribution in a slot returns null the whole row is dropped. The built-in selectedCountPanel() and filteredCountPanel() use this to self-hide.

Stateful counter — combine totals, filter and selection

Section titled “Stateful counter — combine totals, filter and selection”

Because everything you need is on the context object, a single render function can adapt its message to grid state without reading from any plugin directly. The example below shows one combined counter that:

  • shows Total: N rows when nothing is filtered or selected,
  • switches to Filtered: M / N when a filter is active,
  • switches to Selected: S of N when rows are selected,
  • and combines into Selected: S of M / N when both apply.
features: {
pinnedRows: {
slots: [
{
id: 'counter',
position: 'bottom',
render: (ctx) => {
const { totalRows, filteredRows, selectedRows } = ctx;
const isFiltered = filteredRows !== totalRows;
const visible = isFiltered ? `${filteredRows} / ${totalRows}` : `${totalRows}`;
let text: string;
if (selectedRows > 0) {
text = `Selected: ${selectedRows} of ${visible}`;
} else if (isFiltered) {
text = `Filtered: ${visible}`;
} else {
text = `Total: ${totalRows} rows`;
}
const el = document.createElement('span');
el.className = 'tbw-status-panel';
el.textContent = text;
return el;
},
},
],
},
},

The legacy top-level fields are deprecated. Map them to slots as follows:

Legacy fieldSlots equivalent
position: 'top' | 'bottom'Per-slot position field
showRowCount: true{ render: rowCountPanel() }
showSelectedCount: true{ render: selectedCountPanel() }
showFilteredCount: true{ render: filteredCountPanel() }
aggregationRows: [{ ... }]Same shape — drop into slots[] (no render field)
customPanels: [{ render }]Wrap as { render: [{ zone: panel.position, render: panel.render }] }
AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://raw.githubusercontent.com/OysteinAmundsen/toolbox/main/llms-full.txt