Mo Sharif
← ~/blog

Building a ⌘K Command Palette in React: 28+ Shortcuts

I added a command palette to Codelit about three months ago. The users who find it stick around about 3x longer than the ones who don't.

Three times.

A command palette

is a keyboard-triggered search box that exposes every action in an application through one entry point, so a user types the name of a command instead of hunting for it in menus.

Now, some of that is selection bias: the people who go hunting for a ⌘K palette were probably always going to be power users. But it is the feature my stickiest users reach for most, it took a weekend to build, and I have never regretted it.

Power users want a command palette because it collapses every action in the product into one keystroke plus a few characters of typing, which beats any menu hierarchy. They hate menus. They know what they want to do, and they don't want to navigate three levels of dropdowns to do it.

Think about the tools you love using: VS Code, Figma, Linear, Raycast, Notion. They all have command palettes. It's not a coincidence.

For Codelit specifically, the problem was acute. You're working on an architecture diagram on an infinite React Flow canvas. You want to add a database node, switch to a different template, or export to Terraform. Without a command palette, that's: move mouse to toolbar, find the right section, click, find the subsection, click again. With the palette: press two keys, type "ter", hit enter. Done.

Three routes to the same action, and they are not equivalent:

Route to an actionWhat it costs the userBest forWhere it breaks
Nested menus4.2 seconds, averageThe long tail nobody uses twiceDeep hierarchies hide the one thing you need
Command palette1.1 seconds, averageAnything you do occasionallyYou have to roughly know what the action is called
Dedicated shortcutOne or two keysThe top ten actions by frequencyThere are only so many keys, and nobody discovers them

Codelit's palette is built on cmdk by Paco Coursey, with its default filter swapped out for a fuzzy matcher I wrote. cmdk is a headless, composable command palette component for React: no opinions about styling, full keyboard navigation, and it handles the hard parts (focus management, scrolling, accessibility).

Here's the basic setup:

Tsx
import { Command } from "cmdk";

function CommandPalette() {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    const down = (e: KeyboardEvent) => {
      if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        setOpen((open) => !open);
      }
    };
    document.addEventListener("keydown", down);
    return () => document.removeEventListener("keydown", down);
  }, []);

  return (
    <Command.Dialog open={open} onOpenChange={setOpen}>
      <Command.Input placeholder="Type a command or search..." />
      <Command.List>
        <Command.Empty>No results found.</Command.Empty>

        <Command.Group heading="Actions">
          <Command.Item onSelect={() => addNode("database")}>Add Database Node</Command.Item>
          <Command.Item onSelect={() => addNode("service")}>Add Service Node</Command.Item>
          <Command.Item onSelect={() => exportDiagram("terraform")}>
            Export to Terraform
          </Command.Item>
        </Command.Group>

        <Command.Group heading="Navigation">
          <Command.Item onSelect={() => navigate("/templates")}>Go to Templates</Command.Item>
          <Command.Item onSelect={() => navigate("/blog")}>Go to Blog</Command.Item>
        </Command.Group>
      </Command.List>
    </Command.Dialog>
  );
}

That's the skeleton. The real work is in what goes inside it.

Codelit ships 28+ keyboard shortcuts in four groups: canvas editing, view controls, vim-style "go to" navigation, and global toggles. Every one is also reachable by name through the ⌘K palette, so nobody has to memorise the list below. I spent way too long deciding which keys map to which actions.

ShortcutWhat it does
Ctrl+ZUndo
Ctrl+Shift+ZRedo
Ctrl+CCopy selected nodes
Ctrl+VPaste nodes
Delete / BackspaceDelete selected
Ctrl+ASelect all nodes
Ctrl+DDuplicate selected
Ctrl+FFind node on canvas
Ctrl+GGroup selected nodes
ShortcutWhat it does
Ctrl+0Zoom to fit
Ctrl+=Zoom in
Ctrl+-Zoom out
Ctrl+1Zoom to 100%
MToggle minimap
LToggle layout direction (TB/LR)
ShortcutWhat it does
G then TGo to Templates
G then BGo to Blog
G then FGo to Features
G then PGo to Pricing
G then HGo to Home
G then SGo to Settings
ShortcutWhat it does
Cmd+K / Ctrl+KOpen command palette
?Show keyboard shortcuts help
EscapeClose any modal/panel
NNew diagram
EToggle export panel
RToggle review panel
SToggle sidebar
TToggle theme (dark/light)

The single-key toggles earn their keep once Codelit is installed as a standalone app, where there's no browser chrome around the window to fall back on.

Give a dedicated key to any action users perform more than five times per session, and leave the rest to palette search. That was my rule. I tracked action frequency in Codelit for two weeks before building anything, and the data was clear.

  1. Undo/redo, by far the most common action (no surprise)
  2. Add node, second most common
  3. Delete node, third
  4. Zoom to fit, fourth (people lose their diagram on the infinite canvas constantly)
  5. Export, fifth

Everything in the top 10 got a single-key or two-key shortcut. Less frequent actions are accessible through the command palette search but don't have dedicated key bindings.

Codelit's palette reads the app context before it renders and reorders itself around what you're doing. Open it with a node selected and node actions lead; open it on the templates page and template actions do.

When you open itWhat it leads withWhere that comes from
A node is selectedEdit label, Change type, Audit this node, Delete nodecontext.selectedNodes
You're on the canvasAdd database node, Add service node, Run architecture review, Exportcontext.currentPage === "canvas"
You're on the templates pageUse this template, Preview, Forkthe same page check
Any timeYour three most recent actionscontext.recentActions
Typescript
function getContextualActions(context: AppContext): CommandItem[] {
  const actions: CommandItem[] = [];

  if (context.selectedNodes.length > 0) {
    actions.push(
      { label: "Edit Node Label", action: "editLabel" },
      { label: "Audit Node", action: "auditNode" },
      { label: "Change Node Type", action: "changeType" },
      { label: "Delete Selected", action: "deleteSelected" }
    );
  }

  if (context.currentPage === "canvas") {
    actions.push(
      { label: "Add Database Node", action: "addDatabase" },
      { label: "Add Service Node", action: "addService" },
      { label: "Run Architecture Review", action: "review" },
      { label: "Export Diagram", action: "export" }
    );
  }

  // Always include recent actions
  actions.push(...context.recentActions.slice(0, 3));

  return actions;
}

The recent actions bit is key. If you exported to Terraform 5 minutes ago, "Export to Terraform" shows up at the top of the palette next time you open it. Small thing, big impact on perceived speed.

Press ? anywhere in Codelit and you get a full keyboard shortcut reference, organised by category with visual key representations. Discoverability is the main problem with shortcuts — a binding nobody knows about is worth exactly as much as one that doesn't exist. I show a subtle tooltip about the ? modal during onboarding, too.

The palette results carry shortcut hints too. Each item shows its shortcut (if it has one) on the right side. So when someone uses the command palette to "Zoom to fit," they see Ctrl+0 next to it. Next time, they skip the palette and hit the shortcut directly.

This creates a natural learning curve: command palette first, discover shortcut, use shortcut directly. Users graduate from the palette organically.

cmdk's default filter is an exact-substring match, which means typing "terra" won't match "Export to Terraform" if you're off by a character. I replaced it with a custom fuzzy matcher that scores four signals and lets near-misses through.

SignalExampleEffect on ranking
Exact prefix matchexp matches "Export"Highest score, always ranks first
Consecutive character matchtrrfrm matches "Terraform"Survives dropped vowels and typos
Word boundary matchet matches "Export Terraform"First letter of each word counts
Recencyanything you ran a minute agoScore boost on top of text match

The scoring weights are tuned so that exact matches always come first, but typos still find what you're looking for. I got the initial weights from Fuse.js's algorithm, then tweaked them based on actual user search patterns.

Terraform keeps turning up in these examples for a reason. Exporting a diagram to real Terraform and Kubernetes manifests was fifth on that frequency list, and it's a long enough word to be miserable to type.

Three months in: users who use the palette run about 3x longer sessions, 40% of power users (10+ sessions) use shortcuts regularly, and average time to action dropped from 4.2 seconds with menu navigation to 1.1 seconds with the command palette.

Behind those headlines:

  • "Zoom to fit" (Ctrl+0) is the most-used single shortcut in the product.
  • The "every action reachable in under 2 seconds" goal holds for the top 30 actions.
  • The long tail — things like "change edge animation speed" — still takes 3-4 seconds through nested menus, and those are rare enough that it doesn't matter.

If your app has more than ~15 distinct actions, yes. Absolutely. Below that a menu is fine; above it, users stop being able to hold the whole product in their head.

The implementation cost is low. cmdk handles the hard parts. The maintenance cost is near-zero, since you add items to the palette as you add features. And the impact on power user retention is significant.

It's the best weekend I've spent on Codelit, and I've spent a lot of weekends on Codelit.

The users who bother to learn shortcuts are the same users who become your biggest advocates. Make their experience as fast as possible.

Try it at codelit.io. Press Cmd+K or ? and see for yourself.

Questions people actually ask

What is a command palette in a web app?
A command palette is a keyboard-triggered search box that exposes every action in an application through a single entry point. The user presses Command K or Control K, types a few characters of what they want, and hits enter. It replaces a trip through nested menus with one keystroke and a search query, which is why tools like VS Code, Figma, Linear and Notion all ship one.
Which React library should I use to build a command palette?
I used cmdk by Paco Coursey for Codelit's palette. It is a headless, composable React component, so it has no opinions about styling but handles the parts that are genuinely hard: focus management, scrolling the list as you arrow through it, and accessibility. I kept all of that and replaced only its default filter, which is a substring match, with a custom fuzzy matcher so near-miss typing still finds the right command.
How do you decide which actions get a keyboard shortcut?
My rule for Codelit is that any action users perform more than five times per session gets a dedicated shortcut, and everything else lives in the palette's search results. I tracked action frequency for two weeks before building anything. Undo and redo came first, then adding a node, deleting a node, zoom to fit, and export. The top ten actions by volume took the single-key and two-key bindings, and nothing else did.
How do users discover keyboard shortcuts they do not know about?
Discoverability is the main problem with keyboard shortcuts, because a binding nobody knows about is the same as a binding that does not exist. Codelit solves it three ways. Pressing the question mark key anywhere opens a full shortcut reference grouped by category. Onboarding shows a quiet tooltip pointing at it. And every result in the palette displays its own shortcut beside it, so people graduate from searching to pressing the key.
Is a command palette worth building for a small SaaS product?
If your product has more than roughly fifteen distinct actions, yes. Codelit's palette took a weekend to build because cmdk handles the difficult parts, and maintenance is close to zero because you add an entry when you add a feature. Average time to action fell from 4.2 seconds through menus to 1.1 seconds through the palette, and the users who adopt it run about three times longer sessions, though that last figure is correlation rather than proof.