Animated developer command palette with layout-aware selection pill, staggered search results, Up/Down/Enter/Escape keyboard handling, and focus management.
Preview
⌘
Customize
4 props0.035
Installation
Copies the component source and its CSS Module into your project.
npx motionfoundry add morphing-command-paletteWrites components/ui/morphing-command-palette.tsx and components/styles/morphing-command-palette.module.css.
Usage
Updates live as you change the controls — only props that differ from the defaults are included.
example.tsx
1import { MorphingCommandPalette } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <MorphingCommandPalette />6 );7}Props
Accessibility
Implements combobox/listbox keyboard navigation (ArrowUp, ArrowDown, Enter, Escape), automatic input focus on open, and reduced-motion transition fallback.
Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.
Source
components/ui/morphing-command-palette.tsx
1"use client";2 3import * as React from "react";4import { motion, AnimatePresence, useReducedMotion } from "motion/react";5import styles from "../styles/morphing-command-palette.module.css";6 7export interface CommandPaletteAction {8 id: string;9 label: string;10 shortcut: string;11 group: string;12}13 14export interface MorphingCommandPaletteProps {15 /** Input placeholder */16 placeholder?: string;17 /** Initial search query */18 defaultQuery?: string;19 /** Result stagger interval in seconds */20 staggerResults?: number;21 /** Initial open state */22 isOpen?: boolean;23 /** Action items */24 actions?: CommandPaletteAction[];25 /** Selection callback */26 onSelectAction?: (action: CommandPaletteAction) => void;27 /** Optional className */28 className?: string;29}30 31const DEFAULT_ACTIONS: CommandPaletteAction[] = [32 { id: "add-kinetic", label: "npx motionfoundry add kinetic-type", shortcut: "⌘1", group: "CLI" },33 { id: "add-glass", label: "npx motionfoundry add liquid-glass-card", shortcut: "⌘2", group: "CLI" },34 { id: "add-dock", label: "npx motionfoundry add magnetic-dock", shortcut: "⌘3", group: "CLI" },35 { id: "docs-api", label: "Open Component API Reference", shortcut: "⌘D", group: "Docs" },36];37 38export function MorphingCommandPalette({39 placeholder = "Filter commands, components, or actions...",40 defaultQuery = "",41 staggerResults = 0.035,42 isOpen: isOpenProp = true,43 actions = DEFAULT_ACTIONS,44 onSelectAction,45 className = "",46}: MorphingCommandPaletteProps) {47 const [open, setOpen] = React.useState(isOpenProp);48 const [query, setQuery] = React.useState(defaultQuery);49 const [activeIndex, setActiveIndex] = React.useState(0);50 const [lastExecuted, setLastExecuted] = React.useState<string | null>(null);51 const prefersReducedMotion = useReducedMotion();52 const layoutId = React.useId();53 54 React.useEffect(() => {55 setOpen(isOpenProp);56 }, [isOpenProp]);57 58 React.useEffect(() => {59 setQuery(defaultQuery);60 }, [defaultQuery]);61 62 const filtered = React.useMemo(() => {63 const q = query.trim().toLowerCase();64 if (!q) return actions;65 return actions.filter(66 (a) => a.label.toLowerCase().includes(q) || a.group.toLowerCase().includes(q)67 );68 }, [actions, query]);69 70 const handleKeyDown = (e: React.KeyboardEvent) => {71 if (e.key === "Escape") {72 setOpen(false);73 } else if (e.key === "ArrowDown") {74 e.preventDefault();75 setActiveIndex((prev) => Math.min(prev + 1, Math.max(0, filtered.length - 1)));76 } else if (e.key === "ArrowUp") {77 e.preventDefault();78 setActiveIndex((prev) => Math.max(prev - 1, 0));79 } else if (e.key === "Enter" && filtered[activeIndex]) {80 e.preventDefault();81 const chosen = filtered[activeIndex];82 setLastExecuted(chosen.label);83 onSelectAction?.(chosen);84 }85 };86 87 return (88 <div className={`${styles.container} ${className}`.trim()}>89 {!open && (90 <button91 type="button"92 onClick={() => setOpen(true)}93 className={styles.reopenBtn}94 >95 Reopen Command Palette (⌘K)96 </button>97 )}98 <AnimatePresence mode="wait">99 {open && (100 <motion.div101 role="dialog"102 aria-label="Command palette"103 initial={prefersReducedMotion ? { opacity: 1 } : { opacity: 0, scale: 0.96, y: 8 }}104 animate={{ opacity: 1, scale: 1, y: 0 }}105 exit={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, scale: 0.96, y: 6 }}106 transition={{ type: "spring", stiffness: 320, damping: 26 }}107 onKeyDown={handleKeyDown}108 className={styles.palette}109 >110 <div className={styles.inputRow}>111 <span className={styles.promptGlyph}>⌘</span>112 <input113 type="text"114 value={query}115 onChange={(e) => {116 setQuery(e.target.value);117 setActiveIndex(0);118 }}119 placeholder={placeholder}120 aria-label="Command search input"121 className={styles.input}122 />123 <button124 type="button"125 onClick={() => setOpen(false)}126 className={styles.escBadge}127 >128 ESC129 </button>130 </div>131 132 <div role="listbox" aria-label="Command results" className={styles.list}>133 {filtered.map((item, idx) => {134 const selected = idx === activeIndex;135 return (136 <motion.button137 key={item.id}138 type="button"139 role="option"140 aria-selected={selected}141 initial={prefersReducedMotion ? false : { opacity: 0, y: 6 }}142 animate={{ opacity: 1, y: 0 }}143 transition={{ delay: idx * staggerResults, duration: 0.22 }}144 onMouseEnter={() => setActiveIndex(idx)}145 onClick={() => {146 setLastExecuted(item.label);147 onSelectAction?.(item);148 }}149 className={styles.row}150 >151 {selected && (152 <motion.span153 layoutId={`mf-cmd-pill-${layoutId}`}154 className={styles.highlightPill}155 transition={{ type: "spring", stiffness: 360, damping: 28 }}156 />157 )}158 <span className={styles.rowText}>{item.label}</span>159 <kbd className={styles.shortcut}>{item.shortcut}</kbd>160 </motion.button>161 );162 })}163 </div>164 165 {lastExecuted && (166 <div className={styles.footer}>Executed: {lastExecuted}</div>167 )}168 </motion.div>169 )}170 </AnimatePresence>171 </div>172 );173}