gridcn1.2.2

Keybindings

The data-grid-keybindings add-on: an in-app shortcuts dialog that always reflects the effective keymap, plus binding chips for your own UI.

data-grid-keybindings displays the grid's effective keymap as an in-app dialog (always reflecting the actual bindings of your grid, including keymap overrides). The core keyboard model and the default binding table live on Selection & keyboard. This page covers the add-on's display surface.

pnpm dlx shadcn@latest add @gridcn/data-grid-keybindings

Click a cell to focus the grid, then press ? (shift+/) to open the shortcuts dialog — or use the button below. Mod+S runs the custom Save row command (listed in the dialog under Editing); its result shows below.

Name
Role
Score
Silent Tiger
Viewer
4
Quick Lion
Viewer
89
Swift Wolf
Manager
80
Eager Lion
User
51
Eager Tiger
Viewer
83
Silent Wolf
User
15

The dialog

The dialog reads the grid's keymap and labels through the store, so it mounts inside DataGridProvider. The ? shortcut reads the root's container element, so it mounts inside DataGridRoot:

import { useState } from "react";
import {
  DataGridKeybindingsDialog,
  DataGridKeybindingsShortcut,
} from "@/components/data-grid-keybindings/data-grid-keybindings";

function OrdersGrid() {
  const [shortcutsOpen, setShortcutsOpen] = useState(false);

  return (
    <DataGridProvider {...grid} columns={columns}>
      <DataGridKeybindingsDialog open={shortcutsOpen} onOpenChange={setShortcutsOpen} />
      <DataGridRoot>
        <DataGridHeader />
        <DataGridBody />
        <DataGridKeybindingsShortcut onOpen={() => setShortcutsOpen(true)} />
      </DataGridRoot>
    </DataGridProvider>
  );
}

The ? shortcut is a contested key

While DataGridKeybindingsShortcut is mounted and the grid has focus, it claims ? (and only outside an open cell editor): type-to-edit and any keymap binding on ? are shadowed, because the shortcut stops the native event before the grid sees it. Without the shortcut component, ? type-to-edits into the active cell like any other printable character.

mod resolves to Cmd on macOS, iOS, and iPadOS, and to Ctrl elsewhere. Copy, Cut, and Paste (Ctrl/Cmd+C/X/V) are native browser clipboard events, not part of the keymap, but the dialog lists them together for completeness.

Plugin bindings, such as Find and Find and replace from the find-and-replace add-on, appear in the dialog New. A keymap entry for the same action replaces the plugin binding.

Binding chips

BindingChips renders one binding string ("mod+c") as a row of Kbd chips ("⌘" "C"), the same tokens the dialog uses, for your own tooltips or legend rows:

import { BindingChips } from "@/components/data-grid-keybindings/data-grid-keybindings";

<BindingChips binding="mod+shift+ArrowUp" />

For plain text, the core's formatShortcut(binding) (the data-grid entry point, below) returns the same tokens as an array (["⌘", "C"]).

Tokens are platform-aware (mod → ⌘ on macOS, iOS, and iPadOS, Ctrl elsewhere), and a binding renders as one keycap per token.

Shortcut labels for your own UI

The core exports the same token logic for labels outside the add-on. formatShortcut(binding) returns the display tokens for a binding string. formatShortcut("mod+shift+z") is ["⌘", "⇧", "Z"] on macOS and ["Ctrl", "Shift", "Z"] elsewhere. isMacPlatform() reports the platform check the labels use. Both come from the data-grid entry point:

import { formatShortcut, isMacPlatform } from "@/components/data-grid/data-grid";

Modifier tokens render as glyphs (⌘ ⇧ ⌥) on macOS and iOS, and as word names (Ctrl, Shift, Alt) everywhere else. ctrl always renders as Ctrl, including on macOS.

The platform check reads navigator, which does not exist during server rendering. There the labels render as word names. BindingChips renders word names on the server and switches to glyphs after hydration. For your own labels, compute them after mount, or pass the platform as the second argument: formatShortcut(binding, isMac).

Remapping

The keymap is a plain data table, the keymap prop, merged over DEFAULT_KEYMAP. You can remap or extend any binding without touching the engine:

<DataGridProvider {...grid} columns={columns} keymap={{ selectAll: ["mod+shift+a"] }}>
  <DataGridRoot />
</DataGridProvider>

keymap lives on DataGridProvider (or the DataGrid wrapper), not on DataGridRoot. This overrides only the actions you specify. Everything else falls back to DEFAULT_KEYMAP.

// extend the default set: keep mod+a, add a second binding on top
<DataGrid keymap={{ selectAll: ["mod+a", "mod+shift+a"] }} />

// disable an action entirely (empty list = unbound, its keys become no-ops)
<DataGrid keymap={{ selectColumn: [] }} />

A binding string is a modifier prefix plus a key. Combine the modifiers mod, ctrl, shift, and alt in that order ("mod+shift+ArrowUp"). mod resolves to Cmd on macOS, iOS, and iPadOS, and to Ctrl elsewhere. ctrl matches the literal physical Ctrl on every platform. Use it where Cmd is reserved by the OS, for example selectColumn on Ctrl+Space.

The key is one of:

  • a letter, lowercase in the binding (matching is case-insensitive),
  • a digit,
  • a function key, F1 to F24,
  • a named key: ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Home, End, PageUp, PageDown, Enter, Escape, Tab, Backspace, Delete,
  • any single printable character,
  • the space bar, written as a literal space ("shift+ ", " ").

Other stable named keys (Insert, PrintScreen, MediaPlayPause) bind the same way; the list above is the typed set. The plus key cannot be bound: the last + in a binding is the separator between modifiers and key.

For custom binding arrays, the exported KeyBinding type names one binding string:

import { type KeyBinding } from "@/components/data-grid/data-grid";

const extraBindings: KeyBinding[] = ["mod+shift+a", "mod+shift+s"];

A binding the matcher cannot match warns in the browser console in development.

The matcher is event-source-agnostic: the same keymap config drives the in-grid handler, the ? shortcut resolution, and the opt-in global shortcuts layer. When you remap an action, every surface uses the new binding.

The type-to-replace trigger (typeToReplace) is remapped the same way. By default it has no binding: any unbound printable key on the active cell dispatches it and opens the editor seeded with the typed character (Excel behavior). Defining it in keymap takes the action over exclusively. ANY typeToReplace binding suppresses the implicit printable-key fallback entirely, there is no additive mode: keymap={{ typeToReplace: ["F3"] }} means type-to-replace only happens on F3 (a plain edit start), and keymap={{ typeToReplace: [] }} disables it entirely. A printable key bound to typeToReplace (e.g. "a") starts the edit seeded with that character.

Action labels in the dialog (and the per-GridAction label map) come from labels.keybindings (see Internationalization: coverage by group).

Custom commands

A keymap binding that names no built-in action resolves to the provider's commands prop. Each entry is a GridCommand with run, an optional when gate, an optional scope, and optional label/group for the dialog:

<DataGridProvider
  {...grid}
  columns={columns}
  keymap={{ saveRow: ["mod+s"] }}
  commands={{
    saveRow: {
      run: (ctx) => save(ctx.row),
      when: (ctx) => ctx.cell !== null,
      label: "Save row",
      group: "editing",
    },
  }}
>
  <DataGridRoot />
</DataGridProvider>

GridCommand has these members:

PropTypeDefaultDescription
run(ctx: GridCommandCtx) => voidrequiredRuns when a bound key fires.
when(ctx: GridCommandCtx) => booleannoneReturn false to ignore the key entirely.
scope"grid" | "editing" | "always""grid"When the command dispatches. See below.
labelstringhumanized nameThe command's row label in the dialog.
groupCommandGroup"other"The command's section in the dialog.

run and when receive a GridCommandCtx:

PropTypeDescription
apiDataGridApiAn equivalent handle with the methods useDataGridApi returns.
cellCellRef | nullThe active cell as a stable { rowId, columnId } ref, or null.
viewRowIndexnumber | nullThe view row index of the active cell, or null.
viewColumnIndexnumber | nullThe view column index of the active cell, or null.
rowTData | undefinedThe data row of the active cell, or undefined.
readOnlybooleanThe grid's effective readOnly flag.

On an unloaded row of a lazy grid, cell is a placeholder ref. api cannot resolve it.

scope selects when the command dispatches:

  • "grid" dispatches while no edit session is open.
  • "editing" dispatches while an edit session is open. This includes editors without a text input, such as the select list and the date calendar.
  • "always" dispatches in both cases.

An "editing" command never takes over a grid binding. While no edit session is open, the grid keeps its built-in bindings and type-to-edit on that key.

An "editing" command bound to Enter, Escape, or Tab never runs from the built-in editors. The editor commits or cancels on that key first.

Other rules:

  • A name that shadows a built-in action (selectAll, for example) wins over it. Dev mode warns once.
  • A key combo bound to two or more actions resolves to one action. The most specific binding wins. On a tie, a keymap binding beats a default binding. A keymap entry that repeats a default binding counts as the default. Dev mode warns once per shared combo.
  • A bound name with neither a built-in handler nor a commands entry is a no-op. Dev mode warns once.

Name the action by declaration-merging the GridActions interface (the same path as GridCellTypes): the merged name becomes a valid GridAction, so your IDE lists it in keymap, and commands accepts it as a plain key:

declare module "@/components/data-grid/types" {
  interface GridActions {
    saveRow: true;
  }
}

On this page