--- name: implementing-command-palettes description: Use when building Cmd+K command palettes in React - covers keyboard navigation with arrow keys, keeping selected items in view with scrollIntoView, filtering with shortcut matching, and preventing infinite re-renders from reference instability --- # Implementing Command Palettes ## Overview Command palettes (Cmd+K / Ctrl+K) need precise keyboard navigation, scroll behavior, and stable references to avoid re-render loops. This skill covers the mechanical patterns that make command palettes feel responsive. ## When to Use - Building a Cmd+K command palette in React - Implementing arrow key navigation with visual selection - Keeping selected items visible during keyboard navigation - Filtering commands by label text AND keyboard shortcuts - Experiencing infinite re-renders when commands update ## Quick Reference | Feature | Implementation | | ----------------- | ---------------------------------------------------------- | | Arrow navigation | Track `selectedIndex`, clamp with `Math.min/max` | | Keep in view | `scrollIntoView({ block: 'nearest', behavior: 'smooth' })` | | Shortcut matching | Strip spaces from shortcuts, match against query | | Stable icons | Define icon elements outside component | | Stable handlers | `useCallback` + `noop` constant for disabled states | ## Keyboard Navigation ### Index Management ```tsx const [selectedIndex, setSelectedIndex] = useState(0); useEffect(() => { if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { switch (e.key) { case 'ArrowDown': e.preventDefault(); // Clamp to last item setSelectedIndex((prev) => Math.min(prev + 1, filteredItems.length - 1)); break; case 'ArrowUp': e.preventDefault(); // Clamp to first item setSelectedIndex((prev) => Math.max(prev - 1, 0)); break; case 'Enter': e.preventDefault(); if (filteredItems[selectedIndex]) { executeCommand(filteredItems[selectedIndex]); close(); } break; case 'Escape': e.preventDefault(); close(); break; } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isOpen, filteredItems, selectedIndex, close]); ``` **Key patterns:** - `e.preventDefault()` stops arrow keys from scrolling the page - `Math.min/max` prevents index going out of bounds - Effect depends on `filteredItems` so navigation updates when filter changes ### Reset Selection on Query Change ```tsx { setQuery(e.target.value); setSelectedIndex(0); // Reset to first item when query changes }} /> ``` ## Keeping Selected Item in View ### Using Refs Array ```tsx const itemRefs = useRef<(HTMLButtonElement | null)[]>([]); // Scroll effect - runs when selection changes useEffect(() => { const selectedItem = itemRefs.current[selectedIndex]; if (selectedItem) { selectedItem.scrollIntoView({ block: 'nearest', // Minimal scroll - only scroll if needed behavior: 'smooth', // Smooth animation }); } }, [selectedIndex]); // Assign refs in render { filteredItems.map((item, index) => ( )); } ``` ### Alternative: Single Ref for Selected Item ```tsx const selectedItemRef = useRef(null); useEffect(() => { if (isOpen && selectedItemRef.current) { selectedItemRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth', }); } }, [isOpen, selectedIndex]); // Only assign ref to selected item