Row pinning (top / bottom)
Pin specific rows to the top or bottom of the grid so they stay visible while the rest of the data scrolls. Typical use cases:
- A totals or aggregate row anchored at the top of a long ledger.
- An industry benchmark or target row for at-a-glance comparison.
- A page totals row at the bottom that recomputes as filters narrow the visible set.
- A headline metric (e.g. "Closing balance") that should never scroll out of view.
Props
<SvGrid> accepts two props:
pinnedTopRows?: ReadonlyArray<TData>
pinnedBottomRows?: ReadonlyArray<TData>
Each is an array of your own row objects (same TData type as data). The grid renders them inside the same table as the regular rows, with position: sticky so they track the scroll. They share the column schema (widths, pinning, format, cellClass) - you do not redefine columns.
Quick start
<script lang="ts">
import { SvGrid, tableFeatures, rowSortingFeature, type ColumnDef } from 'sv-grid-community'
type Row = { id: string; account: string; arr: number; seats: number }
let rows = $state<Row[]>([ /* ...200 accounts... */ ])
// Pinned-top: a single "totals" row computed from `rows`.
const totals = $derived<Row[]>([
{
id: '⌃ TOTALS',
account: `All ${rows.length} accounts`,
arr: rows.reduce((s, r) => s + r.arr, 0),
seats: rows.reduce((s, r) => s + r.seats, 0),
},
])
const features = tableFeatures({ rowSortingFeature })
const columns: ColumnDef<typeof features, Row>[] = [
{ field: 'id', header: 'Account', width: 130, editable: false },
{ field: 'account', header: 'Name', width: 240, editable: false },
{ field: 'arr', header: 'ARR', width: 140, align: 'right',
format: { type: 'number', options: { style: 'currency', currency: 'USD' } } },
{ field: 'seats', header: 'Seats', width: 100, align: 'right' },
]
</script>
<SvGrid
data={rows}
columns={columns}
features={features}
pinnedTopRows={totals}
/>
The totals row sticks under the header and stays in view while the user scrolls.
Filter-aware "page totals" at the bottom
The grid exposes api.getDisplayedRows() - the post-filter / post-sort / post-pagination row set. Combine it with pinnedBottomRows for a "page total" that updates as filters narrow the data.
<script lang="ts">
let api = $state<SvGridApi<typeof features, Row> | null>(null)
let snapshot = $state<readonly Row[]>([])
// Resync the snapshot whenever filters / sorts likely changed. A
// 400 ms poll keeps the demo simple; production code can hook the
// `onFiltersChange` / `onSortingChange` events instead.
$effect(() => {
const id = setInterval(() => { snapshot = api?.getDisplayedRows() ?? rows }, 400)
return () => clearInterval(id)
})
const pageTotals = $derived<Row[]>([{
id: '⌄ PAGE',
account: `Visible page (n = ${snapshot.length})`,
arr: snapshot.reduce((s, r) => s + r.arr, 0),
seats: snapshot.reduce((s, r) => s + r.seats, 0),
}])
</script>
<SvGrid
data={rows}
columns={columns}
features={features}
pinnedBottomRows={pageTotals}
onApiReady={(next) => (api = next)}
/>
Multiple pinned rows
Pass more than one row in either array; the grid stacks them. The first row sticks closest to the header (top) or to the bottom edge (bottom).
<SvGrid
data={rows}
columns={columns}
features={features}
pinnedTopRows={[totals, benchmark, target]}
pinnedBottomRows={[pageTotals, runningBalance]}
/>
The built-in CSS supports stacking up to 4 rows per side. For larger stacks, override the CSS variables yourself:
.sv-grid-pinned-row-top[data-pinned-index="4"] td {
top: calc(var(--sg-thead-h, 36px) + var(--sg-pinned-row-h, 32px) * 4);
}
How pinned rows behave
| Behavior | Regular rows | Pinned rows |
|---|---|---|
| Inline editing | Yes (when enableInlineEditing) |
No |
| Selection checkbox | Yes (when enabled) | No (cell rendered empty) |
| Cell selection rectangle | Yes | No (cells skipped) |
| Sorting | Yes | No (pinned rows are in a separate <tbody>) |
| Filtering | Yes | No (pinned rows are not filtered) |
| Fill handle | Yes | No |
cellClass |
Yes | Yes |
format |
Yes | Yes |
| Column pinning (left/right) | Yes | Yes |
Pinned rows are read-only by design - they represent aggregates or annotations, not transactional data. If you need an editable row at the top, render it OUTSIDE the grid as a separate toolbar, or use a regular row with data and filter it to always sort first.
Styling
Pinned rows expose two CSS variables on the grid shell that you can override:
.sv-grid-shell {
--sg-pinned-bg: color-mix(in oklab, #6366f1 4%, #ffffff);
--sg-pinned-border: color-mix(in oklab, #6366f1 24%, transparent);
}
For per-row styling, target the row's class directly:
.sv-grid-pinned-row-top { font-weight: 700; }
.sv-grid-pinned-row-bottom { background: #ecfdf5; }
Each row also carries data-pinned-row="top" | "bottom" and data-pinned-index="0..N" so you can paint distinct ranks differently.
See also
- Demo 108: Pinned rows (engine prop)
- Demo 107: Pinned rows (stacked-grids user-land pattern)
api.getDisplayedRows()- drives filter-aware page totals.