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.
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,
F1toF24, - 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:
| Prop | Type | Default | Description |
|---|---|---|---|
run | (ctx: GridCommandCtx) => void | required | Runs when a bound key fires. |
when | (ctx: GridCommandCtx) => boolean | none | Return false to ignore the key entirely. |
scope | "grid" | "editing" | "always" | "grid" | When the command dispatches. See below. |
label | string | humanized name | The command's row label in the dialog. |
group | CommandGroup | "other" | The command's section in the dialog. |
run and when receive a GridCommandCtx:
| Prop | Type | Description |
|---|---|---|
api | DataGridApi | An equivalent handle with the methods useDataGridApi returns. |
cell | CellRef | null | The active cell as a stable { rowId, columnId } ref, or null. |
viewRowIndex | number | null | The view row index of the active cell, or null. |
viewColumnIndex | number | null | The view column index of the active cell, or null. |
row | TData | undefined | The data row of the active cell, or undefined. |
readOnly | boolean | The 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
keymapbinding beats a default binding. Akeymapentry 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
commandsentry 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;
}
}Related
- Selection & keyboard: the selection model and the default keymap table
- Two-stage Ctrl+A: the select-all nuance
- Global shortcuts: the opt-in layer that also follows the remapped undo and redo bindings outside the grid