Building a Cmd/Ctrl+K command palette

One binding to open it on both platforms, a scope so nothing else fires while it's open, and an Escape that still works while its own search box is focused.

Opening it: one binding, both platforms

"cmdOrCtrl+k" resolves to metaKey (⌘) on Mac and ctrlKey everywhere else — one binding instead of registering ["ctrl+k", "meta+k"] and hoping you never forget the second one:

command-palette.ts
import { BindKeyboard } from "bind-keyboard";

const bindKeyboard = new BindKeyboard();

bindKeyboard.add("cmdOrCtrl+k", (event) => {
  event.preventDefault(); // Chrome/Firefox's own bookmark-search shortcut
  openPalette();
});

Pause everything else while it's open

A palette is a full-screen overlay — background shortcuts firing underneath it while the user is typing a command is exactly the kind of bug a scope exists to prevent. Tag every app-level binding you want suspended with { scope: "app" }, then flip it off for as long as the palette is open:

command-palette.ts
bindKeyboard.add("cmdOrCtrl+s", save, true, "keydown", {
  scope: "app",
});

function openPalette() {
  paletteEl.hidden = false;
  bindKeyboard.disableScope("app");
}

function closePalette() {
  paletteEl.hidden = true;
  bindKeyboard.enableScope("app");
}

The "cmdOrCtrl+k" binding that opens the palette stays unscoped, so it isn't affected by this — only the bindings you explicitly tag are. See shortcuts inside a modal for the general version of this pattern.

Escape needs to work while the search box is focused

The palette almost certainly has its own text input for fuzzy search — and checkInputElements defaults to true, meaning bindings are suppressed while any input/textarea/contenteditable is focused. That's the right default for most shortcuts, but Escape closing the palette is exactly the exception allowInInputElements exists for:

command-palette.ts
bindKeyboard.add("escape", closePalette, true, "keydown", {
  allowInInputElements: true,
});

Arrow keys to move the selection

The palette's own up/down navigation and Enter-to-run also need allowInInputElements: true for the same reason — they're meant to work while focus is in the search box, which is the whole point of a palette:

command-palette.ts
bindKeyboard.add("arrowdown", selectNext, true, "keydown", {
  allowInInputElements: true,
});
bindKeyboard.add("arrowup", selectPrevious, true, "keydown", {
  allowInInputElements: true,
});
bindKeyboard.add("enter", runSelected, true, "keydown", {
  allowInInputElements: true,
});

Related

Keyboard shortcuts inside a modal · React keyboard shortcuts: the useKeybind hook