Context menu
The data-grid-context-menu add-on: header dropdown and right-click menus for sort, pin, autosize, hide, and row operations.
data-grid-context-menu installs two menus: a header dropdown (and a right-click header menu)
for column controls, and a right-click cell menu carrying the row-operation items. It reads
grid state through the store hooks, so it mounts inside DataGridProvider.
pnpm dlx shadcn@latest add @gridcn/data-grid-context-menu
Right-click a cell for cut/copy/paste/clear and row operations; right-click a header (or hover it for the ghost chevron) for sort/pin/hide/autosize.
Header surface
Both the header dropdown and the right-click header menu show the same items:
Sort asc/desc/clear, Pin left/right/unpin, Autosize, Hide. Pin is set with
pin: 'left' | 'right' per column as an initial value and changed at runtime through these
menus (the pin behavior itself is documented on Columns).
import {
DataGridContextMenu,
DataGridHeaderDropdown,
} from "@/components/data-grid-context-menu/data-grid-context-menu";
<DataGridProvider {...grid} columns={columns}>
<DataGridContextMenu>
<DataGridRoot renderHeaderMenu={(ctx) => <DataGridHeaderDropdown {...ctx} />}>
<DataGridHeader />
<DataGridBody />
</DataGridRoot>
</DataGridContextMenu>
</DataGridProvider>;DataGridContextMenu must be inside DataGridProvider
It reads grid state through the same selector hooks that your own components use. Mounting
it outside DataGridProvider throws immediately.
The header dropdown opens by keyboard too. Shift+Alt+ArrowDown (the
openColumnMenu action) opens the menu of the active cell's column. Arrow keys move through
the items, and Escape returns focus to the cell.
Cell surface: row-operation items
The row-operation items act on every view row covered by the current selection, falling
back to the right-clicked row when nothing is selected, so a range selection duplicates or
deletes all of its rows at once. Every mutating item (everything but Copy) is disabled on a
readOnly grid. Right-clicking a cell makes it the active cell with a single-cell selection
before the menu opens, so the items always act on a known target. If a Cut or Copy write
fails, the next time the menu opens it disables both items with the
labels.contextMenu.clipboardFailed tooltip. After that menu closes, the items are enabled again.
The cell-surface items, in order:
- Cut / Copy / Paste: the native clipboard pipeline (see Clipboard).
- Clear contents. Empties the selection to each type's
clearValue()(theclearContentsaction, Delete/Backspace). - Insert row above (Ctrl/Cmd+Shift+U) / Insert
row below (Ctrl/Cmd+Shift+F). Anchored at the
right-clicked row. Hidden without
createRow. - Duplicate row(s): Ctrl/Cmd+Shift+X. Hidden
without
duplicateRow. - Delete row(s): Ctrl/Cmd+Shift+Backspace, destructive item.
Surfaces with no menu: row markers, the empty grid space below the last row, and the scrollbar gutter right-click to nothing at all: no menu and no browser context menu either, matching the quiet no-op of a plain click on empty space. The full operation contract (when items appear under which props) lives on Row operations.
Custom menu items
The menus are data. Each surface resolves its items through a resolver prop on
DataGridContextMenu that receives the built-in set as a GridMenuItemSpec[] array
(defaults) and returns the complete item set. Return defaults unchanged, filter it, reorder
it, extend it, or build a fully custom set.
| Prop | Surface | Called with |
|---|---|---|
getCellMenuItems | the right-click cell menu | (ctx, defaults). ctx is { rowId, viewRowIndex, columnId, canInsertRow, canDuplicateRow }: the right-clicked cell's stable row id, its view row, its column id, and the createRow/duplicateRow availability flags the built-in set gates its Insert/Duplicate items on |
getHeaderMenuItems | the right-click header menu AND every DataGridHeaderDropdown inside the wrapper | (ctx, defaults). ctx is { columnId, scrollRoot }: the right-clicked column's id and the grid's scroll-container element (null when the right-clicked element has no grid ancestor) |
CellMenuItemsResolver and HeaderMenuItemsResolver are the function types of getCellMenuItems and getHeaderMenuItems.
The built-in entries carry stable ids, so a resolver keeps or drops them by id:
| Surface | Built-in ids, in order |
|---|---|
| Cell | cut, copy, paste, sep-clipboard, clear-contents, sep-rows, insert-row-above, insert-row-below, duplicate-rows, delete-rows (the Insert entries appear only with createRow, the Duplicate entry only with duplicateRow) |
| Header | sort-asc, sort-desc, clear-sort, sep-sort, pin-left, pin-right, unpin, hide-column, sep-autosize, autosize, autosize-all (clear-sort only while the column is sorted, unpin only while it is pinned, the autosize items only while resizing is enabled) |
GridMenuItemSpec is an item or a separator. An item carries id, label, icon (a React
node), shortcut (a keymap binding rendered as keycaps), disabled, destructive, tooltip
(hover hint while disabled), and onClick. A separator is { id, separator: true }.
The menus are data: each resolver receives the built-in item set and returns the kept entries by id. Right-click a cell for the two kept row items; the header dropdown and the right-click header menu both keep only Sort ascending.
Keep two row items for the cell surface, and one sort item for both header surfaces:
import { DataGridContextMenu } from "@/components/data-grid-context-menu/data-grid-context-menu";
<DataGridContextMenu
getCellMenuItems={(_ctx, defaults) =>
defaults.filter((s) => s.id === "insert-row-below" || s.id === "duplicate-rows")
}
getHeaderMenuItems={(_ctx, defaults) => defaults.filter((s) => s.id === "sort-asc")}
>
<DataGridRoot>
<DataGridHeader />
<DataGridBody />
</DataGridRoot>
</DataGridContextMenu>Extend the built-in set with a custom item. The resolver closes over hook values that a component above it already holds:
import { Plus } from "lucide-react";
import { useDataGridActions } from "@/components/data-grid/data-grid";
import { DataGridContextMenu } from "@/components/data-grid-context-menu/data-grid-context-menu";
function GridWithExtraItem() {
const actions = useDataGridActions();
return (
<DataGridContextMenu
getCellMenuItems={(ctx, defaults) => [
...defaults,
{
id: "new-row-here",
label: "New row here",
icon: <Plus className="size-4 text-muted-foreground" />,
onClick: () => actions.insertRow(ctx.viewRowIndex, "below"),
},
]}
>
<DataGridRoot>
<DataGridHeader />
<DataGridBody />
</DataGridRoot>
</DataGridContextMenu>
);
}The resolver runs during the wrapper's render
The resolver is a plain function call, not a component. Do not call hooks in the resolver body. Capture hook values in the enclosing component and close over them, as above.
Touch
NewLong-press a cell or a header for 500 ms to open its menu. The grid raises one contextmenu
event, so onCellContextMenu fires as it does for a right-click. The browser's own touch menu
stays closed. When the fill add-on is mounted, the cell menu of a touch selection also lists
Fill down and Fill right. Their text comes from labels.contextMenu.fillDown and
labels.contextMenu.fillRight.
Paste permission
Requires the clipboard-read permission
The Paste item needs clipboard-read. Browsers can refuse this without a user gesture inside
the menu item itself. When permission is not available, the item degrades to a disabled state
with a "press Ctrl+V" hint, instead of failing silently.
The paste semantics themselves (TSV/HTML formats, anchored expand, tiling) are documented on Clipboard.
Internationalization
All menu item labels come from the labels prop. labels.contextMenu carries the header
items and the row-operation items, shared verbatim between the right-click menu and the header
dropdown. See Internationalization: coverage by group.
Related
- Columns: pin left/right: the pin behavior the menus control
- Row operations: the operations behind the cell-surface items
- Clipboard: the Cut/Copy/Paste pipeline