Skip to content

Row actions

col.actions(cell) adds a narrow column for a per-row control, usually a ⋯ menu. The grid renders it outside the drag-to-reorder machinery so a menu anchored in it opens where it should.

Click a row.

Zara SharmaDesignInvited$0.00
Kavya MüllerDataPaused$0.00
Olivia MüllerDesignInvited$0.00
Omar IyerSalesPaused$0.00
Elias GarciaFinanceChurned$0.00
Chloe RossiResearchPaused$1,276.00
Sofia GarciaDesignActive$48.00
Idris BoseSupportInvited$48.00
Noah BosePlatformActive$0.00
Zara SharmaResearchActive$1,595.00
40 rows
Page 1 of 4

Pass a cell renderer. It gets the usual cell context, so row.original is your object and row is the TanStack row with pin, toggleSelected, toggleExpanded and the rest.

import {
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
col.actions(({ row }) => (
<span onClick={(e) => e.stopPropagation()}>
<DropdownMenu modal={false}>
<DropdownMenuTrigger render={<Button variant="ghost" size="icon-xs" aria-label="Row actions" />}>
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => edit(row.original)}>Edit</DropdownMenuItem>
<DropdownMenuItem onClick={() => row.pin(row.getIsPinned() ? false : "top")}>
{row.getIsPinned() ? "Unpin row" : "Pin row to top"}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onClick={() => remove(row.original)}>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</span>
))

Put it last in the columns. The column is 56px wide, right-aligned, has an empty header and the id actions; a second argument takes any TanStack column option (size, header, enablePinning, …).

A modal menu locks page scrolling and puts a backdrop over everything else while it is open. Inside a grid that is its own scroll area, with sticky headers and pinned columns, that is more than you want from a row menu: non-modal keeps the page and the grid scrollable, and the menu still closes on an outside click or Esc. The grid’s own column menus are non-modal for the same reason.

Headers are draggable for reordering. A draggable column carries a CSS transform while it moves and re-registers with the drag sensor as the grid changes, which gives a popup anchored in one of its cells an unstable position. The utility kinds (select, expander, rowNumber, actions) are rendered outside that machinery, so a menu in col.actions positions reliably. If another column’s cell opens a popover, give that column stable: true.

React events bubble through portals, so a click on a menu item reaches the row’s onClick as well. The stopPropagation wrapper above keeps a menu click from counting as a row click; the grid’s own checkbox, link and expander cells do the same.

row.pin("top"), row.pin("bottom") and row.pin(false) need row pinning on:

<DataGrid rowPinning />

A pinned row sticks under the header (or above the pager) and stays there while you scroll, sort, filter and page. See row pinning.

<DataGrid
onRowClick={(row, event) => router.push(`/customers/${row.original.id}`)}
getRowClassName={(row) => (row.original.status === "churned" ? "opacity-60" : undefined)}
/>

onRowClick(row, event) fires for a click anywhere on the row that nothing inside stopped. It runs before the built-in behaviour; call event.preventDefault() to stop a click from toggling selection on a grid with no checkbox column and cell selection off.

getRowClassName(row) returns extra classes for the <tr>. It composes with striped, the selected tint (data-state="selected"), the grouped-row style and the pinned-row style. Rows also carry data-row-id, data-index, data-grouped and data-pinned for CSS.

  • The actions column is never sorted, filtered, searched, grouped, hidden, resized or exported. It can be pinned: initialState={{ columnPinning: { end: ["actions"] } }} keeps it in view on a wide grid.
  • Cell selection skips the utility columns, so arrowing across a row stops before the menu, and the menu button is reached with Tab.
  • Row pinning is off by default. Without rowPinning, row.pin changes state the table does not render.
Option Type Default What it does
col.actions builder col.actions(cell, options?): cell gets the cell context; options is any TanStack column option.
stable (column) boolean false; true for utility kinds Render outside the reorder machinery.
rowPinning boolean false Let rows be pinned with row.pin.
onRowClick function (row, event) => void: a click on the row.
getRowClassName function (row) => string: extra classes for the row, or nothing.
striped boolean false Tint every other row.
labels.rowActions string "Row actions" For your row menu’s aria-label; the grid’s own header menus use labels.columnMenu.