MynduiMyndui
0

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.

01/04Backdrop, panel, 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);}
01

Backdrop, panel, rows

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.

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.

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.

02/04Enter and exit spring
enter
hold
exit
spring
spring
scale .96 → 1, blur 8 → 0
settled, opacity 1
scale .97, blur 6px
tsx
<motion.divinitial={{ 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 }}/>
02

Enter and exit 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.

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).

03/04One highlight

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}
03

One highlight

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.

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.

04/04Result

Open the palette and run a command

04

The result

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.

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.

Motion Score

Command PaletteSSCompositor-only
SopacityFade / cross-fade
SfilterFilter (blur / brightness)
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →