Column pinning
Pin a column and it stays in view while the rest of the grid scrolls sideways. The checkbox and the row-actions menu are the usual candidates: one at the start, one at the end.
| Chloe Bose | chloe.bose@mail.test | Active | Free | Support | Intern | $0.00 | 1 | 65.6% | Feb 21, 2023 | — | Canada | ||
| Ishaan Garcia | ishaan.garcia@mail.test | Active | Pro | Platform | Manager | $36.00 | 3 | 40.3% | May 11, 2025 | — | India | ||
| Ananya Iyer | ananya.iyer@corp.example | Active | Enterprise | Platform | Lead | $1,422.00 | 18 | 8.3% | May 26, 2024 | — | Germany | ||
| Diya Sharma | diya.sharma@example.com | Active | Enterprise | Design | Director | $3,950.00 | 50 | 99.6% | Jun 12, 2023 | — | Canada | ||
| Noah Prabhakaran | noah.prabhakaran@mail.test | Paused | Free | Data | Manager | $0.00 | 1 | 90.4% | Dec 7, 2023 | Speaks at meetups. | France | ||
| Elias Smith | elias.smith@mail.test | Paused | Pro | Finance | Analyst | $36.00 | 3 | 11.2% | Mar 31, 2026 | Wants SSO. | India | ||
| Elias Kim | elias.kim@mail.test | Active | Enterprise | Design | PM | $1,975.00 | 25 | 17.4% | Jun 18, 2026 | Asked about invoices. | France | ||
| Kwame Dubois | kwame.dubois@team.dev | Invited | Free | Support | Intern | $0.00 | 1 | 75.3% | May 12, 2023 | Wants SSO. | Australia | ||
| Freya Tanaka | freya.tanaka@mail.test | Active | Free | Growth | Analyst | $0.00 | 1 | 9.4% | Feb 10, 2023 | Needs onboarding call. | Nigeria | ||
| Elias Müller | elias.muller@corp.example | Paused | Free | Sales | Designer | $0.00 | 1 | 82.2% | Nov 11, 2023 | — | Canada |
0 of 60 row(s) selected.
Page 1 of 6
import { MoreHorizontalIcon } from "lucide-react";
import { Button } from "@/components/ui/button";import { DataGrid, createDataGridColumns } from "@/components/ui/data-grid";
import { makePeople, PLAN_OPTIONS, STATUS_OPTIONS, type Person,} from "@/lib/data-grid-demo-data";
const col = createDataGridColumns<Person>();
const columns = col.columns([ col.select(), col.text("name", { header: "Name", size: 180 }), col.text("email", { header: "Email", size: 240 }), col.enum("status", { header: "Status", options: STATUS_OPTIONS, size: 120 }), col.enum("plan", { header: "Plan", options: PLAN_OPTIONS, size: 120 }), col.text("team", { header: "Team", size: 140 }), col.text("role", { header: "Role", size: 140 }), col.currency("mrr", { header: "MRR", currency: "USD", size: 120 }), col.number("seats", { header: "Seats", size: 90 }), col.percent("usage", { header: "Usage", size: 100 }), col.date("joinedAt", { header: "Joined", size: 130 }), // Its menu has no "Pin" items. col.text("notes", { header: "Notes", size: 220, enablePinning: false }), col.text("country", { header: "Country", size: 130 }), col.actions(({ row }) => ( <Button variant="ghost" size="icon-xs" aria-label={`Actions for ${row.original.name}`}> <MoreHorizontalIcon /> </Button> )),]);
const data = makePeople(60, 13);
/** Checkbox and Name pinned to the start, the actions menu to the end; scroll sideways and they stay. */export default function DataGridPinning() { return ( <DataGrid key="pinning" data={data} columns={columns} getRowId={(row) => row.id} initialState={{ columnPinning: { start: ["select", "name"], end: ["actions"] } }} /> );}Use it
Section titled “Use it”Open a column’s menu (the ⋯ button at the right of its header, shown on hover and focus, or right-click the header) and choose Pin to start or Pin to end. A pinned header shows a pin icon, and so does its row in the View popover. Unpin sends it back.
To start pinned, set initialState.columnPinning:
<DataGrid data={data} columns={columns} initialState={{ columnPinning: { start: ["name"], end: ["actions"] } }}/>If the grid has a checkbox column, pin it too (start: ["select", "name"]); otherwise the pinned
name moves ahead of it.
From code, the TanStack API is one call away:
grid.table.getColumn("name")?.pin("start") // or "end", or false to unpingrid.table.setColumnPinning({ start: ["name"], end: [] })grid.table.resetColumnPinning()How it behaves
Section titled “How it behaves”- Start and end, not left and right. TanStack Table v9 pins to logical regions. In a
left-to-right layout
startis the left edge; underdir="rtl"it is the right edge. The grid positions pinned cells withinset-inline-startandinset-inline-end, so a pinned column lands on the correct side in both directions with nothing extra from you. - Sticky, not split. The grid is one
<table>. A pinned column’s header and cells getposition: stickywith an offset equal to the widths of the pinned columns before it (column.getStart("start")on the start side,column.getAfter("end")on the end side). Nothing is cloned into a second table, so sorting, filtering, selection, keyboard navigation and editing work in a pinned column exactly as in any other.getPinningStyles(column, { header })andpinningClasses(column)are exported if you render cells of your own. - Order. Start-pinned columns come first, in the order they were pinned; then the unpinned (“centre”) columns in their column order; then the end-pinned columns. Pinning appends the column to its region; unpinning returns it to its place in the centre order. Exports and the clipboard follow this on-screen order.
- The edge. The last start-pinned column and the first end-pinned column draw a one-pixel, border-coloured inset shadow on their inner edge, and every pinned cell has an opaque background, so scrolling columns pass behind them rather than through them.
- Pinned columns don’t drag. Drag-to-reorder works within the centre region. A pinned header has no drag handle and its menu hides Move left and Move right; unpin it to move it.
- Resizing a pinned column shifts the offsets of the pinned columns after it. The footer row,
when a column defines a
footer, is pinned the same way. - Hiding a pinned column keeps it in the pinning state; show it again and it comes back pinned.
- Group headers (
col.group) pin all of their leaf columns together. - Persistence.
columnPinningis part of the view the grid remembers withpersist, and of every saved view. Reset view in the View popover clears it.
Options
Section titled “Options”Column options (any column builder, or columnDef):
| Option | Type | Default | What it does |
|---|---|---|---|
enablePinning |
boolean |
true |
false removes the pin items from this column’s menu (column.getCanPin() is false). |
Grid options:
| Option | Type | Default | What it does |
|---|---|---|---|
initialState.columnPinning |
ColumnPinningState |
empty | Which columns start pinned, by id, in order. A persisted view wins over it. |
table.enableColumnPinning |
boolean |
true |
Turns the feature off for every column (through the table escape hatch). |
table.state.columnPinning |
ColumnPinningState |
Control the slice yourself, paired with table.onColumnPinningChange. |
Helpers exported from the grid:
| Export | What it does |
|---|---|
getPinningStyles(column, options?) |
The width, sticky position, inline inset and z-index for a cell of that column; pass { header: true } for a header cell. |
pinningClasses(column) |
The opaque background and the edge shadow classes for a pinned cell. |
See also
Section titled “See also”- Column reordering: why pinned columns don’t drag.
- Row pinning: the same idea for rows.
- Saved views and persistence: remembering what the user pinned.