Build a Command Palette: Cmd+K Like Linear and Vercel

Cmd+K command palettes have become standard UX for power-user-friendly apps — Linear, Vercel, GitHub, Slack, Raycast all have them. The interview tests whether you understand the combobox pattern, fuzzy search, keyboard interactions, and the ergonomics that distinguish a great command palette from a mediocre one.

Functional requirements

  • Cmd+K (or Ctrl+K) opens the palette
  • Type to filter commands
  • Arrow keys to navigate; Enter to execute
  • Fuzzy search (typos and partial matches)
  • Recent commands surface at top
  • Keyboard shortcuts shown next to commands
  • Categories (Navigation, Actions, Help)

Architecture

Three pieces:

  1. Command registry: all commands the app supports
  2. Palette UI: input + results list (modal)
  3. Keyboard handler: global Cmd+K trigger

Library: cmdk

The de facto React command palette library by Vercel. Used by Linear, Vercel, Raycast, Sourcegraph. Headless — bring your own styles.

For interview answers, knowing it exists and using it as the model is appropriate. For learning, building from scratch is instructive.

The command registry

type Command = {
  id: string;
  label: string;
  description?: string;
  icon?: ReactNode;
  shortcut?: string[]; // ['cmd', 'k']
  category: string;
  action: () => void;
  keywords?: string[]; // for search
};

Commands registered globally (or per-context). Filter by current page when palette opens.

Use a library:

  • Fuse.js: classic, configurable
  • uFuzzy: faster, modern
  • command-score: small, tailored for command palettes

cmdk uses command-score. Tuned for the “user types prefix or substring” pattern.

Keyboard handling

  • Cmd+K: open palette (also Ctrl+K on Windows/Linux)
  • Escape: close
  • Arrow Up/Down: navigate
  • Enter: execute
  • Cmd+1-9: jump to nth result (some apps)

Search-as-you-type

Filter on every keystroke. With command-score, even 1000 commands filter in under 5ms.

Highlight matching characters in results for visual confirmation.

Recent commands

Track last 5–10 used. Show at top when palette opens with no input. Boosts power users dramatically.

Persist to localStorage; sync across devices via your backend if useful.

Sub-palettes

Some palettes have nested levels:

  • Type “/” → switch to slash commands
  • Select “Move issue” → next level shows projects

Visual: breadcrumb at top of palette (“Move issue ›”).

Loading async commands

Some results require API calls (search across all docs):

  • Show loading spinner
  • Debounce 200ms
  • Cancel previous request
  • Cache recent results

Mobile

Mobile keyboards lack Cmd. Use:

  • Search button in top bar
  • Long-press on a button to reveal palette
  • Slide-up sheet instead of centered modal

Accessibility

  • Modal pattern: focus trap, Escape closes, return focus
  • aria-activedescendant for highlighted option
  • aria-expanded on input
  • aria-live for “X results found”

Common antipatterns

  • Doesn’t open quickly (palette should appear in <100ms)
  • Search blocks main thread on every keystroke
  • No keyboard shortcuts beyond arrows and Enter
  • Confused state when many results match
  • Recent commands not surfaced

Frequently Asked Questions

Should I use cmdk or build from scratch?

cmdk for production. Building from scratch is good interview practice and instructive.

How do I prevent palette from showing in textareas?

Check event.target — if it is an input/textarea, only open if the user explicitly typed Cmd+K (not just K).

Can the palette do more than navigate?

Yes — Linear and Notion let you create issues, assign people, change status, all from the palette. Treat it as a command surface.

Scroll to Top