GodUIGodUI
81

Anatomy of the Command Palette

How a flat groups array becomes a spring-loaded ⌘K menu, with a single highlight that springs between rows instead of restyling them.

A command palette is three unglamorous jobs wearing one glossy interaction: mount an overlay, filter a list as you type, and keep a keyboard cursor in sync with the mouse. None of that is hard on its own — what makes it feel fast is that the filtering, the keyboard, and the ⌘K shortcut all share one piece of state, and the "active" row is one animated element, not a re-render of every row.

Backdrop, panel, rows

CommandPalette portals a fixed-position layer into document.body and renders it only while open is true, via AnimatePresence. Behind the panel sits a click-to-close backdrop; inside it, a search input drives a useMemo'd filter over groups, and the surviving items render flat, one <button role="menuitem"> per row:

Anatomybackdrop → panel → search → rows
backdrop
click-through scrim, closes on click
panel
role=dialog, max-h-[60vh]
search
autofocused, filters as you type
item
icon · label · shortcut
active
highlight fill behind the hot row
tsx
 
const filteredGroups = groups
  .map((g) => ({ ...g, items: g.items.filter((i) => matches(i, query)) }))
  .filter((g) => g.items.length > 0);

function matches(item: CommandItem, query: string) {
  if (!query) return true;
  const q = query.toLowerCase();
  return (
    item.label.toLowerCase().includes(q) ||
    item.keywords?.some((k) => k.toLowerCase().includes(q)) === true
  );
}

matches checks the label and an optional keywords array, so an item titled "Toggle Theme" can still surface on "dark" or "light" — the palette searches intent, not just the string on screen.

Enter and exit share one spring

The panel doesn't fade in from nowhere — it resolves out of a blur, like a lens racking into focus. initial/animate/exit move four properties at once (opacity, scale, y, filter: blur()), all on the same spring:

Enter / exitstiffness 320 · damping 32 · mass 0.9
enter
hold
exit
spring
spring
scale .96 → 1, blur 8 → 0
settled, opacity 1
scale .97, blur 6px
tsx
 
<motion.div
  initial={{ opacity: 0, scale: 0.96, y: -8, filter: "blur(8px)" }}
  animate={{ opacity: 1, scale: 1, y: 0, filter: "blur(0px)" }}
  exit={{ opacity: 0, scale: 0.97, y: -6, filter: "blur(6px)" }}
  transition={{ type: "spring", stiffness: 320, damping: 32, mass: 0.9 }}
/>

The exit isn't a mirror of the entrance — scale 0.97 and y -6 are a shorter throw than 0.96/-8, and the blur is 6px instead of 8. Dismissing reads as a quick settle rather than the entrance played backward, which matters because exits happen far more often than opens (every Escape, every selection, every backdrop click).

One highlight, not one per row

The active-row indicator isn't a class toggled on whichever row is hot — it's a single motion.span that only ever exists inside one row at a time, bound by a shared layoutId:

Active rowlayoutId binds one pill to the hot row

layoutId="command-active" · spring 500/35 · re-targets on ArrowUp/Down or mouse move

tsx
 
{isActive ? (
  <motion.span
    layoutId="command-active"
    transition={{ type: "spring", stiffness: 500, damping: 35 }}
    className="absolute inset-0 rounded-lg bg-accent"
  />
) : null}

When activeIndex changes — ArrowDown/ArrowUp, or onMouseMove re-targeting the row under the cursor — the span it was rendered inside unmounts and a new one mounts in the new row. Framer notices they share layoutId, diffs the old box against the new one, and springs that transform instead of cross-fading two static rects. The spring is stiffer than the panel's own enter (500/35 vs 320/32) because it has to keep up with a held-down arrow key without lagging behind it.

Keyboard, ⌘K, and cleanup

ArrowDown/ArrowUp wrap with a modulo so the cursor never runs off the list, Enter selects whatever flat[activeIndex] currently is, and Escape closes:

tsx
 
if (e.key === "ArrowDown") {
  setActiveIndex((i) => (i + 1) % Math.max(flat.length, 1));
} else if (e.key === "ArrowUp") {
  setActiveIndex((i) => (i - 1 + Math.max(flat.length, 1)) % Math.max(flat.length, 1));
} else if (e.key === "Enter") {
  const item = flat[activeIndex];
  if (item) select(item);
}

With enableShortcut (the default), a document-level keydown listener toggles onOpenChange(!open) on ⌘K / Ctrl+K — bound and torn down in one effect keyed on [enableShortcut, open, onOpenChange], so it's live even before the palette has ever opened. A separate effect keyed on open autofocuses the input via requestAnimationFrame (so it fires after the portal paints) and locks document.body.style.overflow, restoring the previous value on close — a background page can't scroll under an open palette, and it never leaks the lock if the component unmounts mid-session.

The result

Resultthe real component — try ⌘K / Ctrl+K

Open the palette and run a command

One groups array, filtered in place, walked flat, with a single animated highlight riding on top of it. The panel is a spring; the rows are just data.

On this page

Built with GodUI

Beautifully crafted motion components for modern interfaces.

Star on GitHub