Skip to content

Row pinning

Pin a row and it stays at the top or the bottom of the grid while the others scroll, and stays there through sorting, filtering and paging. There is no built-in button for it: you call row.pin from your own row-actions menu.

Idris KhanChurnedSales$0.00Jul 13, 2026
Zara NguyenPausedSales$0.00Aug 6, 2024
Olivia TanakaActiveResearch$638.00Nov 30, 2025
Kavya TanakaActiveDesign$0.00Aug 1, 2025
Olivia PrabhakaranActiveResearch$4,266.00Sep 25, 2026
Mia DasActiveSupport$36.00Apr 9, 2024
Liam NguyenInvitedSupport$0.00Apr 3, 2024
Omar DasInvitedFinance$36.00Jun 28, 2024
Omar KhanActiveSupport$0.00Apr 4, 2025
Aarav PrabhakaranActiveData$0.00Apr 12, 2025
Yuki TanakaPausedSales$0.00May 2, 2023
Diya PrabhakaranActiveGrowth$928.00Aug 19, 2023
Arjun MenonActiveGrowth$1,421.00Jan 18, 2024
Olivia MüllerActiveDesign$4,977.00May 16, 2024
Freya GarciaActivePlatform$48.00Oct 13, 2023
Noah MüllerPausedSales$203.00Nov 25, 2025
Elias OkaforActivePlatform$0.00Feb 21, 2025
Kavya SilvaInvitedDesign$1,738.00Nov 8, 2023
Rohan RossiActiveSales$12.00Oct 10, 2025
Noah PrabhakaranActiveGrowth$3,792.00Jul 6, 2024
Amelia DasChurnedGrowth$0.00Jan 2, 2023
Haruto MüllerActiveSupport$36.00Nov 1, 2024
Zara NairActivePlatform$667.00Sep 12, 2025
Diya SmithInvitedSales$24.00Jun 21, 2026
Sofia ReddyPausedDesign$0.00Sep 16, 2024
Ananya NguyenActiveData$24.00Jan 10, 2023
Diya KimInvitedResearch$48.00Nov 4, 2023
Mateo DuboisActiveSales$24.00Oct 1, 2026
Olivia IyerActiveData$12.00Sep 20, 2024
Vihaan IyerInvitedSupport$1,624.00Jan 14, 2025
Lucas GarciaActiveFinance$261.00May 12, 2025
Kwame DasInvitedDesign$48.00May 25, 2026
Meera SilvaActivePlatform$36.00Jul 26, 2026
Haruto SmithActiveSales$48.00Mar 19, 2023
Leila PrabhakaranInvitedPlatform$3,950.00Jan 21, 2025
Theo GarciaPausedFinance$1,501.00Feb 28, 2026
Nia BoseChurnedPlatform$0.00Oct 31, 2024
Diya KimActiveDesign$12.00Oct 4, 2026
Theo NguyenActiveData$1,422.00Jul 18, 2025
Vihaan RossiInvitedFinance$0.00Mar 8, 2024

Turn it on with rowPinning, then call row.pin("top" | "bottom" | false) from a row-actions menu; row.getIsPinned() tells you the current state so the menu can offer the right items:

col.actions(({ row }) => {
const pinned = row.getIsPinned() // "top" | "bottom" | false
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger render={<Button variant="ghost" size="icon-xs" aria-label="Row actions" />}>
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{pinned !== "top" && <DropdownMenuItem onClick={() => row.pin("top")}>Pin row to top</DropdownMenuItem>}
{pinned !== "bottom" && <DropdownMenuItem onClick={() => row.pin("bottom")}>Pin row to bottom</DropdownMenuItem>}
{pinned && <DropdownMenuItem onClick={() => row.pin(false)}>Unpin row</DropdownMenuItem>}
</DropdownMenuContent>
</DropdownMenu>
)
})
<DataGrid data={data} columns={columns} getRowId={(row) => row.id} rowPinning height={400} />

Start with rows pinned through initialState.rowPinning: { top: [ids], bottom: [ids] }. From code: table.setRowPinning(...), table.resetRowPinning(), table.getTopRows(), table.getCenterRows(), table.getBottomRows(). labels.pinRowTop, pinRowBottom and unpinRow hold the suggested wording.

  • Where they render. The body is the top-pinned rows, then the centre rows (the page, or every row without a pager), then the bottom-pinned rows. A top-pinned row is position: sticky just under the header, with a border along its bottom edge; a bottom-pinned row sticks to the bottom of the scroll area with a border along its top. Both have an opaque background and carry data-pinned="top" or "bottom" for your styling.

  • They survive filters and pages. The grid sets keepPinnedRows: true: a pinned row stays visible when a filter would remove it and shows on every page. Pass table={{ keepPinnedRows: false }} to let filters and paging hide it like any other row.

  • Trees. row.pin(position, includeLeafRows, includeParentRows) can take a row’s descendants or its ancestors along.

  • Not remembered. Row pinning is about particular rows, not the user’s view of the grid, so it is not part of the persisted view and resets on reload. Control table.state.rowPinning with table.onRowPinningChange if you want to keep it.

  • Everything else works in a pinned row: selection, editing, cell selection, the row menu.

  • Pinned rows live outside the virtual list. With virtualize they are always in the DOM and stay stuck at their edge however far the rest scrolls.

  • Several per edge stack. Each top-pinned row sticks just below the ones pinned before it, and each bottom-pinned row just above the ones after it (and above the footer, when there is one).

Grid options:

Option Type Default What it does
rowPinning boolean false Enables row.pin and the sticky rendering.
initialState.rowPinning RowPinningState empty Rows pinned at the start, by id: { top: ["p1"], bottom: [] }.
table.enableRowPinning boolean or predicate rowPinning (row) => boolean decides per row (row.getCanPin()).
table.keepPinnedRows boolean true Show pinned rows even when filtered or paged out.

Row api (TanStack): row.pin(position, includeLeafRows?, includeParentRows?), row.getIsPinned(), row.getCanPin(), row.getPinnedIndex().