Commands and menus

GridCommand: the one implementation behind a menu item, a toolbar button and the API — the built-in set, app commands and overrides, the context they run in, and the menu model resolveMenu draws from.

Last updated November 3, 2026

A command is the one implementation behind a menu item, a toolbar button and a renderer's runCommand — "Sort ascending", "Hide column", "Clear filters". Whether it shows and whether it can run are predicates over a context, a snapshot of the grid taken where it was asked for, and it changes the grid only through the controller's actions, so a change made from a menu reports through the same callback as the user's own gesture would.

import {
  createCommandContext,
  createGridController,
  defaultMessages,
  resolveCommands,
  runCommand,
} from "@gridkitjs/core";

const controller = createGridController<Order>({
  dataSource: orders,
  columns,
  sortableColumns: true,
});
const commands = resolveCommands<Order>(undefined);

const context = createCommandContext({
  controller,
  target: { area: "header", columnId: "Amount" },
  messages: defaultMessages,
  host: { autoFitColumns() {}, openPanel() {}, announce() {} },
});

const sortAscending = commands.get("sort.ascending");
if (sortAscending !== undefined) {
  runCommand(sortAscending, context); // true — the grid now sorts by Amount
}

Commands#

A GridCommand<Row>:

FieldTypeDescription
idstring"sort.ascending" for a built-in, or any id of your own. Never "separator", which a menu reads as one.
labelstring | (context) => stringWhat a menu item or toolbar button reads.
iconstringA key the renderer maps to an icon of its own. A key it does not know draws nothing.
shortcutstringA key-combination hint in aria-keyshortcuts syntax, such as "Alt+ArrowUp" — shown, not bound.
isVisible(context) => booleanWhether the command shows here at all. Defaults to always.
isEnabled(context) => booleanWhether it can run. A disabled command still shows, so a user can see what is not possible. Defaults to always.
isChecked(context) => booleanPresent for a checkable command — a checkbox item in a menu, a toggle button in a toolbar — and says whether it is checked.
execute(context) => voidWhat it does.

The predicates are asked whenever a menu or toolbar is resolved, so a command follows the grid's state without being told about it.

Targets#

A GridCommandTarget says where a command was asked for. A built-in reads the column, row or group it acts on from here.

TargetWhere
{ area: "header", columnId }A column header.
{ area: "cell", rowId, columnId }A data cell.
{ area: "group", groupId, columnId }A group header row.
{ area: "body" }Empty body space, or a grid with no rows.
{ area: "toolbar", itemId }A toolbar item.
{ area: "grid" }A renderer's API, with no particular place.

The context#

createCommandContext({ controller, target, messages, host }) — a GridCommandContextInput — builds the GridCommandContext<Row> every predicate and execute receive, from the controller's state, view and options as they are now. messages is the grid's messages already laid over the defaults, as resolveMessages returns them. The controller may be of any node type, so a renderer's GridController<Row, ReactNode> serves as it is.

FieldTypeDescription
targetGridCommandTargetWhere the command was asked for.
stateGridState<Row>Every slice of state.
viewGridView<Row, unknown>What the user is looking at.
optionsGridOptions<Row>The grid's options.
messagesGridMessagesThe grid's strings, which every built-in label reads.
actionsGridCommandActions<Row>Every action of the controller — all of it but getOptions, setOptions, subscribe, getState and getView. The only way a command changes the grid.
hostGridCommandHostWhat only the renderer can do — see below.
rowResolvedRow<Row> | nullThe row a cell target is on; null on any other target, or for a row the filter hides.
columnDataColumn<Row, unknown> | nullThe column a header, cell or group target names, hidden or not; null on any other target.
selectedRows() => readonly ResolvedRow<Row>[]The selected rows that pass the filter, in the order they are shown, across every page and collapsed group. Worked out on first call and remembered: it walks every filtered row, and most commands never ask.

A context is a snapshot. A renderer builds one each time it resolves a menu or a toolbar, so its commands see the state the user sees.

The host#

A GridCommandHost holds what a command needs that only a renderer has. A caller with no renderer — a test, a script in Node — passes members that do nothing.

MemberDescription
autoFitColumns(columnIds)Sizes each column to fit its content, as double-clicking its resize handle does.
openPanel(kind, target)Opens one of the grid's panels anchored to the target. kind is a GridPanelKind: "columnChooser" for now.
announce(text)Reads text out through the grid's live region.

Built-in commands#

builtInCommands<Row>() returns the grid's own commands. Each acts on a column, a row, a group or the whole grid, and is hidden on a target that has none of what it needs — so any id can be listed in any menu without checks. Their labels come from messages, and their ids form the BuiltInCommandId type.

IdShown forVisible whenEnabled whenDoesLabel
sort.ascendingheaderthe column is sortablealways; checked while it sorts ascendingSorts by this column alone, ascending.sortAscending
sort.descendingheaderthe column is sortablealways; checked while it sorts descendingSorts by this column alone, descending.sortDescending
sort.clearheaderthe column is sortablethe column is sortedclearColumnSort, keeping the rest of the sort.clearSort
sort.clearAllgrid, toolbar, headeralwaysanything is sortedsetColumnSort([])clearAllSorting
group.addheaderthe column is groupable and not groupedalwaystoggleGroupBy, adding it to the end of the grouping.groupByColumn
group.removeheader, groupthe column is groupedalwaystoggleGroupBy, taking it out of the grouping.removeGroupBy
group.clearAllgrid, toolbaranything is groupedalwayssetGroupBy([])ungroupAll
group.expandAllgrid, toolbar, group, cellanything is groupedalwaysexpandAllGroups()expandAllGroups
group.collapseAllgrid, toolbar, group, cellanything is groupedalwayscollapseAllGroups()collapseAllGroups
column.hideheaderthe column is hideableanother column is shownhideColumns([id])hideColumn
column.autoFitheaderthe column is resizablealwayshost.autoFitColumns([id])autoFitColumn
column.autoFitAllgrid, toolbar, headerany shown column is resizablealwayshost.autoFitColumns(ids) with every shown resizable column.autoFitAllColumns
column.choosegrid, toolbar, headeralwaysalwayshost.openPanel("columnChooser", target)chooseColumns
selection.selectAllRowsgrid, cellselectable.rows is "multiple"there are rowsselectAllRows()selectAllRows
selection.cleargrid, cell, headerrows, columns or cells are selectableanything is selectedclearSelection()clearSelection
filter.clearAllgrid, toolbar, headeralwaysanything filterssetFilter([]), which clears the search too.clearFilters

"Sortable" is the column's sortable, else the grid's sortableColumns; "groupable" the same with groupable and groupableColumns; "resizable" is the column as rendered, after resizableColumns. hideable defaults to true and, like sortable, gates only the user's ways to do it: hideColumns still hides a column that is not hideable.

The sorting, grouping, hiding and autofit built-ins carry an icon key — a BuiltInCommandIcon: sortAscending and sortDescending on the two sorts, group on group.add and group.remove, hide on column.hide, and autoFit on both autofits. @gridkitjs/react draws each as an icon of its own; a command of yours may use the same keys to look like the built-ins, and a renderer of your own maps them to its icons or leaves them out.

The ids filter.open, copy.cell, copy.row, copy.selection, export.csv, export.json, export.xlsx, export.pdf, print and layout.reset are reserved for built-ins still to come.

Panels#

A panel is a small dialog a renderer opens beside where it was asked for — the column chooser now, a column's filter editor later. A command opens one through host.openPanel(kind, target), passing its own target along, so the renderer can anchor it: below a header, beside a toolbar button, or at the grid's corner for { area: "grid" }. The renderer holds which panel is open, one at a time; core holds what each one shows.

The column chooser#

column.choose opens the column chooser: a checkbox for every data column in column order, hidden ones included, a search box narrowing the list, and a "Show all" checkbox over what the search shows. resolveColumnChooser({ columns, visibility, search }) — a ColumnChooserInput — works out what it draws, and what "Show all" does:

import { resolveColumnChooser } from "@gridkitjs/core";

const { dataColumns } = controller.getView();
const { columnVisibility } = controller.getState();
const chooser = resolveColumnChooser({
  columns: dataColumns,
  visibility: columnVisibility,
  search: "date",
});

for (const entry of chooser.entries) {
  drawCheckbox(entry.label, entry.visible, entry.disabled, () =>
    entry.visible
      ? controller.hideColumns([entry.columnId])
      : controller.showColumns([entry.columnId]),
  );
}
const toggle = chooser.toggleAll;
drawShowAll(chooser.selectAll, toggle === null, () => {
  if (toggle?.visible === true) controller.showColumns(toggle.columnIds);
  if (toggle?.visible === false) controller.hideColumns(toggle.columnIds);
});
FieldTypeDescription
entriesreadonly ColumnChooserEntry[]The columns whose header text contains search, ignoring case, in column order: columnId, label (the header text), visible and disabled.
selectAllSelectAllState"Show all"'s state — "checked", "unchecked" or "mixed" — over the entries whose column is hideable: a column the user cannot hide is always on, and not counted.
toggleAllColumnChooserToggle | nullWhat toggling "Show all" does — { visible, columnIds }, the columns to show or to hide — or null when it can do nothing, for a disabled checkbox.

The rules every chooser keeps:

  • An entry is disabled — its checkbox cannot be unchecked — while its column shows and is either not hideable or the last column shown. A hidden column can always be shown, hideable or not.
  • "Show all" shows every hidden column the search lists once any is hidden, and hides those it lists once none is. It never touches a column that is not hideable, and never hides the last column shown: when that is all that would be left, the first of them stays.
  • Changes go through showColumns and hideColumns, so each reports through onColumnVisibilityChange like any other.

App commands and overrides#

resolveCommands(commands) returns every command a grid knows, keyed by id: the built-ins, then yours. A command of yours with a built-in's id replaces it, so every menu and toolbar listing that id gets your version — translated, gated by a privilege, or doing something else entirely — without being changed itself.

import { resolveCommands, type GridCommand } from "@gridkitjs/core";

const exportSelected: GridCommand<Order> = {
  id: "orders.exportSelected",
  label: "Export selected",
  isEnabled: (context) => context.selectedRows().length > 0,
  execute: (context) => {
    exportOrders(context.selectedRows().map((entry) => entry.row));
  },
};

// Replaces the built-in: only an administrator may hide columns.
const hideColumn: GridCommand<Order> = {
  id: "column.hide",
  label: ({ messages }) => messages.hideColumn,
  isVisible: ({ target, column }) =>
    target.area === "header" && column !== null && user.isAdmin,
  execute: ({ column, actions }) => {
    if (column !== null) {
      actions.hideColumns([column.id]);
    }
  },
};

const commands = resolveCommands([exportSelected, hideColumn]);

A command with the id "separator" is left out with a console.error — a menu reads that string as a separator.

runCommand(command, context) runs a command only when it is visible and enabled on the context's target, and returns whether it ran — so a renderer reports only a command that did something, and a call through the API cannot run one the grid's state no longer allows.

FunctionTypeDescription
builtInCommands()readonly GridCommand<Row>[]The grid's own commands, typed for your Row. Two calls build equal ones.
resolveCommands(commands)ReadonlyMap<string, GridCommand<Row>>The built-ins, then commands, keyed by id — yours replacing a built-in of the same id.
createCommandContext(input)GridCommandContext<Row>The context for one target, over the controller as it is now.
runCommand(command, context)booleanRuns the command when visible and enabled, and says whether it did.

On a grid#

A grid takes its app commands through the commands option, which a renderer passes to resolveCommands — so the grid's menus list them by id, and a built-in's id replaced there is replaced in every menu:

<DataGrid
  columns={columns}
  dataSource={orders}
  commands={[exportSelected, hideColumn]}
  contextMenu={{ cell: ["orders.exportSelected", "separator", "selection.clear"] }}
  onCommand={({ commandId, target, source }) => track(commandId, source)}
/>

onCommand is called after a command runs, with a GridCommandEvent — and after an inline menu item runs, with that item's id. A hidden or disabled one, which does not run, is not reported.

FieldTypeDescription
commandIdstringThe command's id, or the inline item's.
targetGridCommandTargetWhere it was asked for — { area: "grid" } for a call through the API with none.
source"contextMenu" | "toolbar" | "api"A GridCommandSource: a context menu, the toolbar, or a renderer's API.

A menu is a list of MenuEntry<Row>s, the one vocabulary every grid menu and toolbar is written in:

EntryDescription
"sort.ascending"A command id — a built-in's or one of yours. Typed as BuiltInCommandId or any string, so the built-ins autocomplete.
"separator"A separator.
{ type: "item", id, label, onSelect, … }A MenuItemEntry: an item written into this menu alone rather than registered as a command.
{ type: "submenu", id, label, items }A MenuSubmenuEntry: an item that opens a menu of its own.

An inline item has a command's fields under the names a menu reads naturally: label, icon and shortcut; hidden, disabled and checked, each a boolean or a function of the context, with checked present only on a checkable item; and onSelect(context) in place of execute. A submenu has a label, an optional icon and its items.

import { resolveMenu, type MenuEntry } from "@gridkitjs/core";

const headerMenu: readonly MenuEntry<Order>[] = [
  "sort.ascending",
  "sort.descending",
  "sort.clear",
  "separator",
  {
    type: "submenu",
    id: "grouping",
    label: "Grouping",
    items: ["group.add", "group.remove", "group.clearAll"],
  },
  "separator",
  {
    type: "item",
    id: "orders.pin",
    label: "Pin column",
    checked: ({ column }) => column !== null && pinned.has(column.id),
    onSelect: ({ column }) => {
      if (column !== null) {
        togglePin(column.id);
      }
    },
  },
];

const entries = resolveMenu(headerMenu, { commands, context });

resolveMenu(entries, { commands, context }) — a MenuResolveOptions — resolves a menu against the grid as the context sees it, and returns the ResolvedMenuEntry[] a renderer draws:

  • A command id becomes its command's item. A hidden command or inline item is left out, and so is an id naming no command, reported once per command map with a console.error.
  • Labels become strings and flags booleans; checked stays undefined for an item that is not checkable.
  • A submenu is resolved the same way, and left out when nothing in it shows.
  • Separators never lead, trail or sit next to another, however the items around them came and went.

An empty result means there is nothing to show, so a renderer shows no grid menu there.

Resolved entryFields
ResolvedMenuItemtype: "item", id, label, icon, shortcut, disabled, checked (boolean | undefined), run()
ResolvedMenuSubmenutype: "submenu", id, label, icon, items — never empty
ResolvedMenuSeparatortype: "separator", key — the separator's place among the entries it came from, unique among its menu's separators

run() runs an item against the context its menu was resolved in — a command through runCommand, so only while it is visible and enabled; an inline item unless it is disabled — and returns whether it ran.

Context menus#

A grid's contextMenu option — boolean | GridContextMenuOptions<Row> — says which context menus it shows, by area. It is off by default, leaving the browser's menu. true shows each area's defaults; an object gives an area's entries in the menu vocabulary above — or a function working them out, below — an area left out keeping its defaults and [] leaving the browser's menu there.

AreaWhereTarget
headerA column header.{ area: "header", columnId }
cellA data cell — a menu for its row and cell.{ area: "cell", rowId, columnId }
groupA group header row.{ area: "group", groupId, columnId }
bodyThe body's empty space, below the last row or with no rows.{ area: "body" }

A GridContextMenuTarget is a command target in one of these areas.

<DataGrid
  columns={columns}
  dataSource={orders}
  sortableColumns
  contextMenu={{
    header: ["sort.ascending", "sort.descending", "separator", "column.hide"],
    cell: ["selection.selectAllRows", "selection.clear"],
  }}
/>

defaultContextMenu holds each area's defaults — each item hidden where it has nothing to act on, and the separators collapsing around what is left:

AreaDefaults
headersort.ascending, sort.descending, sort.clear, a separator, group.add, group.remove, a separator, column.autoFit, column.autoFitAll, a separator, column.hide and column.choose.
cellselection.selectAllRows, selection.clear, a separator, group.expandAll and group.collapseAll.
groupgroup.expandAll, group.collapseAll, a separator and group.remove — which stops grouping by the row's column.
bodyNone: empty space keeps the browser's menu until the grid lists entries for it.

An area takes a GridContextMenuEntries<Row, Area>: its entries, or a function asked each time its menu opens with a GridContextMenuRequest<Row, Area> and returning the entries to show — or null, like [], for no grid menu there.

FieldTypeDescription
targetGridContextMenuTarget<Area>Where the menu opens, typed for the area: a header's carries columnId, a cell's rowId too.
defaultsreadonly MenuEntry<Row>[]The area's defaults, to keep, trim, reorder or add to.
contextGridCommandContext<Row>The grid as the menu sees it — the context its items resolve and run in.
contextMenu={{
  cell: ({ target, defaults, context }) =>
    target.columnId === "Id"
      ? null
      : [
          { type: "item", id: "orders.open", label: "Open", onSelect: ({ row }) => open(row) },
          "separator",
          ...defaults,
        ],
}}

For a renderer of your own, two functions read the option:

FunctionTypeDescription
contextMenuEntries(contextMenu, context)readonly MenuEntry<Row>[]The entries the option gives the place context.target names — asking the area's function, if it has one — for resolveMenu to resolve in the same context. None for a target outside the four areas.
hasContextMenu(contextMenu, area)booleanWhether the option opens a grid menu on area at all: it is on, and the area has entries or a function rather than []. For an aria-keyshortcuts that lists Shift+F10.

No entries, or none left once resolved, means no grid menu there: the renderer leaves the browser's.

import {
  contextMenuEntries,
  createCommandContext,
  resolveMenu,
} from "@gridkitjs/core";

const context = createCommandContext({
  controller,
  target: { area: "header", columnId },
  messages,
  host,
});
const entries = resolveMenu(
  contextMenuEntries(controller.getOptions().contextMenu, context),
  { commands, context },
);
if (entries.length > 0) {
  event.preventDefault();
  showMenu(entries);
}

onContextMenuOpenChange is called as a grid's context menu opens and closes, with a ContextMenuOpenChangeEvent — { open, target }, where the menu opened or where the one closing had. A menu opened in place of another reports that one closing first. The open state itself is the renderer's: no option holds it.

@gridkitjs/react draws them and opens them for you — see its context menu.

Keyboard#

Grid menus follow the WAI-ARIA menu pattern. A disabled item stays focusable, so a screen reader can find it, and does nothing when activated; a checkable item is a menuitemcheckbox with aria-checked.

KeyDoes
ArrowDown / ArrowUpThe next or previous item, wrapping round the ends and skipping separators.
Home / EndThe first or last item.
Enter / SpaceRuns the item and closes every menu; on a submenu item, opens it and focuses its first item.
ArrowRightOn a submenu item, opens it and focuses its first item.
ArrowLeftIn a submenu, closes it and focuses the item that opened it.
EscapeCloses the current menu only; focus goes back one level.
Tab / Shift+TabCloses every menu; focus returns to where the menu was opened from.
A printable characterTypeahead: the next item whose label starts with what was typed.

Hovering over an item moves focus to it, and hovering over a submenu item opens it after a short delay.

The two decisions those keys need are pure, and live here; the behaviour that holds a menu's focus — @gridkitjs/dom's attachMenu — calls them:

FunctionTypeDescription
nextMenuIndex(entries, from, move)numberThe item focus moves to from index from, -1 for none. move is a MenuMove: "next", "previous", "first" or "last". Skips separators, wraps round the ends and counts disabled items; -1 when there is no item.
typeaheadIndex(labels, from, query)numberThe item query picks, matched case-insensitively against the start of each label, wrapping. One letter, or one letter typed again and again, searches from the item after from, so repeating it steps through every item it starts; a longer query keeps the focused item while it still matches. -1 for no match.
Edit this page on GitHub