Command Palette
A Cmd+K style searchable command menu with a filtered live list.
- Category
- Patterns
- Added
- 2026-08-23
- Deps
- 2
- Updated
- 2026-09-01
Props
"use client";
import { useMemo, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Search, ArrowRight } from "lucide-react";
const DEFAULT_ITEMS = [
"Create new component",
"Search the library",
"Copy install command",
"Toggle dark mode",
"Open AI prompt",
];
export function CommandPalette({
items = DEFAULT_ITEMS,
placeholder = "Type a command...",
}: {
items?: string[];
placeholder?: string;
}) {
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
const results = useMemo(
() => items.filter((item) => item.toLowerCase().includes(query.toLowerCase())),
[items, query],
);
return (
<>
<button
onClick={() => setOpen(true)}
className="flex items-center gap-3 rounded-full border border-neutral-200 px-4 py-2.5 text-sm text-neutral-500 hover:border-red-500 hover:text-red-500"
>
<Search size={15} />
Search commands
<kbd className="rounded border border-neutral-200 bg-neutral-50 px-1.5 py-0.5 font-mono text-[10px] text-neutral-400">
⌘K
</kbd>
</button>
<AnimatePresence>
{open && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setOpen(false)}
className="fixed inset-0 z-40 bg-black/40 backdrop-blur-[2px]"
/>
<motion.div
initial={{ opacity: 0, y: -12, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -12, scale: 0.98 }}
transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
className="fixed left-1/2 top-24 z-50 w-full max-w-md -translate-x-1/2 overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-2xl"
>
<div className="flex items-center gap-2 border-b border-neutral-200 px-3 py-2.5">
<Search size={14} className="shrink-0 text-neutral-400" />
<input
autoFocus
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={placeholder}
className="w-full bg-transparent text-sm outline-none placeholder:text-neutral-400"
/>
</div>
<div className="max-h-72 overflow-y-auto py-1">
{results.length === 0 ? (
<p className="px-3 py-3 text-xs text-neutral-400">No commands match.</p>
) : (
results.map((item) => (
<button
key={item}
onClick={() => setOpen(false)}
className="flex w-full items-center justify-between px-3 py-2 text-left text-sm hover:bg-neutral-50"
>
{item}
<ArrowRight size={12} className="text-neutral-400" />
</button>
))
)}
</div>
</motion.div>
</>
)}
</AnimatePresence>
</>
);
}
About this pattern
The trigger button, the Cmd+K hint, and the filtered list are all real, but the keyboard shortcut is not wired up in the shipped code, only described as optional in the accompanying prompt. As shipped, opening the palette requires clicking the trigger; the global keydown listener for Cmd+K/Ctrl+K and Escape-to-close both need adding yourself with a useEffect, easy to assume is already there because the button visibly promises it. Filtering runs as a plain case-insensitive substring match recomputed with useMemo on every keystroke, fine for a few dozen commands but wanting a fuzzy-match library once the list grows into the hundreds. Reach for this over a full library like cmdk when you want a small, fully-owned implementation without its dependency tree; reach for cmdk once you need grouped sections or built-in fuzzy scoring. The overlay animates via AnimatePresence with fixed positioning in the real component; the live preview here fakes that with absolute positioning, since a true fixed overlay would escape the preview's sandboxed frame.
Curator’s note
The live preview scopes the overlay to its own card (absolute, not fixed) since a real fixed-position overlay would escape the sandboxed preview frame entirely — the shipped code above uses real fixed positioning, which is what you want in an actual app.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Searchable FAQ Accordion
patternsA filterable FAQ list where each answer expands independently.
Nested Command Menu
componentsA Cmd+K menu with real submenu drilling, a breadcrumb trail, and scoped per-level search.
Native Sheet Dialog
patternsA sheet on the native dialog element — no hand-rolled focus trap, and the three things the platform leaves you fixed.