Command Palette Modal
Original · freecommand palette modal with search
byWeb InnoventixReact + Tailwind
modalcommandpalettemodals
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/modal-command-palette.jsonmodal-command-palette.tsx
"use client"
import {
useState,
useRef,
useEffect,
useMemo,
useCallback,
useId,
type ReactNode,
type KeyboardEvent,
} from "react"
import { motion, AnimatePresence, useReducedMotion } from "motion/react"
/* ------------------------------------------------------------------ */
/* Inline SVG icons (stroke = currentColor, no icon library) */
/* ------------------------------------------------------------------ */
type IconProps = { className?: string }
function SearchIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.2-3.2" />
</svg>
)
}
function GridIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<rect x="3" y="3" width="7" height="7" rx="1.5" /><rect x="14" y="3" width="7" height="7" rx="1.5" />
<rect x="3" y="14" width="7" height="7" rx="1.5" /><rect x="14" y="14" width="7" height="7" rx="1.5" />
</svg>
)
}
function ChartIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M4 20h16" /><path d="M7 20v-6" /><path d="M12 20V8" /><path d="M17 20v-9" />
</svg>
)
}
function UsersIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<circle cx="9" cy="8" r="3.2" /><path d="M3.5 19a5.5 5.5 0 0 1 11 0" /><path d="M16 6.2a3 3 0 0 1 0 5.6" /><path d="M18 14.5a5.3 5.3 0 0 1 2.5 4.5" />
</svg>
)
}
function CardIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<rect x="3" y="5" width="18" height="14" rx="2.5" /><path d="M3 10h18" /><path d="M7 15h4" />
</svg>
)
}
function PlusIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M12 5v14" /><path d="M5 12h14" />
</svg>
)
}
function RocketIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M5 15c-1.2 1.4-1.6 4-1.6 4s2.6-.4 4-1.6" /><path d="M9 15l-.9-.9a2 2 0 0 1 0-2.8l4-4A7 7 0 0 1 20 4a7 7 0 0 1-3.3 7.9l-4 4a2 2 0 0 1-2.8 0Z" /><circle cx="14.5" cy="9.5" r="1.4" />
</svg>
)
}
function KeyIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<circle cx="8" cy="15" r="4" /><path d="m11 12 8-8" /><path d="m17 6 2 2" /><path d="m14 9 2 2" />
</svg>
)
}
function MailIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<rect x="3" y="5" width="18" height="14" rx="2.5" /><path d="m4 7 8 6 8-6" />
</svg>
)
}
function SwapIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M17 3v14" /><path d="m21 13-4 4-4-4" /><path d="M7 21V7" /><path d="m3 11 4-4 4 4" />
</svg>
)
}
function LinkIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M9.5 14.5 14.5 9.5" /><path d="M8 12 6 14a3.5 3.5 0 0 0 5 5l2-2" /><path d="M16 12l2-2a3.5 3.5 0 0 0-5-5l-2 2" />
</svg>
)
}
function LogoutIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3" /><path d="M10 12H3" /><path d="m6 8-4 4 4 4" />
</svg>
)
}
function BookIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M4 5a2 2 0 0 1 2-2h12v16H6a2 2 0 0 0-2 2Z" /><path d="M18 3v18" /><path d="M8 7h6" /><path d="M8 11h6" />
</svg>
)
}
function KeyboardIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<rect x="2.5" y="6" width="19" height="12" rx="2.5" /><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M8 14h8" />
</svg>
)
}
function LifeBuoyIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<circle cx="12" cy="12" r="9" /><circle cx="12" cy="12" r="4" /><path d="m5.6 5.6 3.2 3.2M15.2 15.2l3.2 3.2M18.4 5.6l-3.2 3.2M8.8 15.2l-3.2 3.2" />
</svg>
)
}
function CloseIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="m6 6 12 12M18 6 6 18" />
</svg>
)
}
function EnterIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M20 6v5a3 3 0 0 1-3 3H5" /><path d="m8 11-3 3 3 3" />
</svg>
)
}
/* ------------------------------------------------------------------ */
/* Command data */
/* ------------------------------------------------------------------ */
type Command = {
id: string
label: string
hint: string
section: string
keywords: string[]
shortcut?: string[]
icon: ReactNode
}
const COMMANDS: Command[] = [
{ id: "nav-overview", label: "Go to Overview", hint: "Project health at a glance", section: "Navigation", keywords: ["home", "dashboard", "start"], shortcut: ["G", "O"], icon: <GridIcon className="h-4 w-4" /> },
{ id: "nav-analytics", label: "Go to Analytics", hint: "Traffic, funnels & retention", section: "Navigation", keywords: ["stats", "metrics", "reports", "charts"], shortcut: ["G", "A"], icon: <ChartIcon className="h-4 w-4" /> },
{ id: "nav-team", label: "Go to Team Members", hint: "Roles, seats & permissions", section: "Navigation", keywords: ["people", "users", "members", "roles"], shortcut: ["G", "T"], icon: <UsersIcon className="h-4 w-4" /> },
{ id: "nav-billing", label: "Go to Billing & Plans", hint: "Invoices, usage & upgrades", section: "Navigation", keywords: ["payments", "invoice", "subscription", "plan"], shortcut: ["G", "B"], icon: <CardIcon className="h-4 w-4" /> },
{ id: "new-project", label: "Create New Project", hint: "Spin up a fresh workspace", section: "Create", keywords: ["add", "start", "workspace"], shortcut: ["N", "P"], icon: <PlusIcon className="h-4 w-4" /> },
{ id: "new-deploy", label: "Trigger Deployment", hint: "Ship the current branch to production", section: "Create", keywords: ["ship", "release", "publish", "build"], shortcut: ["N", "D"], icon: <RocketIcon className="h-4 w-4" /> },
{ id: "new-invite", label: "Invite Teammate", hint: "Send an email invitation", section: "Create", keywords: ["add member", "share", "collaborator"], shortcut: ["N", "I"], icon: <MailIcon className="h-4 w-4" /> },
{ id: "new-key", label: "Generate API Key", hint: "Create a scoped access token", section: "Create", keywords: ["token", "secret", "credential", "auth"], shortcut: ["N", "K"], icon: <KeyIcon className="h-4 w-4" /> },
{ id: "acc-workspace", label: "Switch Workspace", hint: "Jump between organizations", section: "Account", keywords: ["org", "change", "team", "tenant"], shortcut: ["⌘", "\\"], icon: <SwapIcon className="h-4 w-4" /> },
{ id: "acc-copy", label: "Copy Current URL", hint: "Grab a shareable link", section: "Account", keywords: ["link", "share", "clipboard"], shortcut: ["⌘", "C"], icon: <LinkIcon className="h-4 w-4" /> },
{ id: "acc-signout", label: "Sign Out", hint: "End this session", section: "Account", keywords: ["logout", "exit", "leave"], icon: <LogoutIcon className="h-4 w-4" /> },
{ id: "help-docs", label: "Read Documentation", hint: "Guides, API reference & recipes", section: "Help", keywords: ["docs", "manual", "learn", "guide"], shortcut: ["?"], icon: <BookIcon className="h-4 w-4" /> },
{ id: "help-shortcuts", label: "Keyboard Shortcuts", hint: "Browse every hotkey", section: "Help", keywords: ["hotkeys", "keys", "bindings"], shortcut: ["⌘", "/"], icon: <KeyboardIcon className="h-4 w-4" /> },
{ id: "help-support", label: "Contact Support", hint: "Chat with a human in minutes", section: "Help", keywords: ["help", "assist", "ticket", "email"], icon: <LifeBuoyIcon className="h-4 w-4" /> },
]
/* ------------------------------------------------------------------ */
/* Match highlighting */
/* ------------------------------------------------------------------ */
function highlight(text: string, query: string): ReactNode {
const q = query.trim()
if (!q) return text
const idx = text.toLowerCase().indexOf(q.toLowerCase())
if (idx === -1) return text
return (
<>
{text.slice(0, idx)}
<mark className="rounded-[3px] bg-amber-200/80 px-0.5 text-inherit dark:bg-amber-400/25">
{text.slice(idx, idx + q.length)}
</mark>
{text.slice(idx + q.length)}
</>
)
}
function Kbd({ children }: { children: ReactNode }) {
return (
<kbd className="inline-flex min-w-[1.4rem] items-center justify-center rounded-md border border-slate-300/80 bg-slate-100 px-1.5 py-0.5 font-sans text-[11px] font-medium text-slate-600 shadow-[0_1px_0_rgba(0,0,0,0.04)] dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">
{children}
</kbd>
)
}
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
export default function CommandPalette() {
const reduce = useReducedMotion()
const uid = useId().replace(/[:]/g, "")
const listId = `${uid}-list`
const inputId = `${uid}-input`
const [open, setOpen] = useState(false)
const [query, setQuery] = useState("")
const [activeIndex, setActiveIndex] = useState(0)
const [toast, setToast] = useState<string | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const triggerRef = useRef<HTMLButtonElement>(null)
const toastTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
const filtered = useMemo<Command[]>(() => {
const q = query.trim().toLowerCase()
if (!q) return COMMANDS
return COMMANDS.filter((c) => {
const hay = `${c.label} ${c.hint} ${c.keywords.join(" ")}`.toLowerCase()
return hay.includes(q)
})
}, [query])
const grouped = useMemo(() => {
const out: { section: string; items: { cmd: Command; index: number }[] }[] = []
filtered.forEach((cmd, index) => {
let group = out.find((g) => g.section === cmd.section)
if (!group) {
group = { section: cmd.section, items: [] }
out.push(group)
}
group.items.push({ cmd, index })
})
return out
}, [filtered])
const optionId = useCallback((i: number) => `${uid}-opt-${i}`, [uid])
const runCommand = useCallback((cmd: Command) => {
setOpen(false)
setToast(cmd.label)
if (toastTimer.current) clearTimeout(toastTimer.current)
toastTimer.current = setTimeout(() => setToast(null), 2600)
}, [])
// Global open shortcut (Cmd/Ctrl + K)
useEffect(() => {
const onKey = (e: globalThis.KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
e.preventDefault()
setOpen((v) => !v)
}
}
window.addEventListener("keydown", onKey)
return () => window.removeEventListener("keydown", onKey)
}, [])
// Reset + focus + scroll lock + focus restore
useEffect(() => {
if (open) {
setQuery("")
setActiveIndex(0)
const prevOverflow = document.body.style.overflow
document.body.style.overflow = "hidden"
const t = setTimeout(() => inputRef.current?.focus(), 0)
return () => {
clearTimeout(t)
document.body.style.overflow = prevOverflow
triggerRef.current?.focus()
}
}
}, [open])
// Keep active option in view
useEffect(() => {
if (!open) return
const el = document.getElementById(optionId(activeIndex))
el?.scrollIntoView({ block: "nearest" })
}, [activeIndex, open, optionId, filtered.length])
useEffect(() => {
return () => {
if (toastTimer.current) clearTimeout(toastTimer.current)
}
}, [])
const move = useCallback(
(delta: number) => {
const len = filtered.length
if (len === 0) return
setActiveIndex((i) => (i + delta + len) % len)
},
[filtered.length]
)
const onInputKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "ArrowDown") {
e.preventDefault()
move(1)
} else if (e.key === "ArrowUp") {
e.preventDefault()
move(-1)
} else if (e.key === "Home") {
e.preventDefault()
setActiveIndex(0)
} else if (e.key === "End") {
e.preventDefault()
if (filtered.length) setActiveIndex(filtered.length - 1)
} else if (e.key === "Enter") {
e.preventDefault()
const cmd = filtered[activeIndex]
if (cmd) runCommand(cmd)
}
}
const onPanelKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Escape") {
e.preventDefault()
setOpen(false)
return
}
if (e.key === "Tab") {
const root = panelRef.current
if (!root) return
const focusables = Array.from(
root.querySelectorAll<HTMLElement>(
'a[href],button:not([disabled]),input,[tabindex]:not([tabindex="-1"])'
)
).filter((el) => el.offsetParent !== null || el === document.activeElement)
if (focusables.length === 0) return
const first = focusables[0]
const last = focusables[focusables.length - 1]
if (e.shiftKey && document.activeElement === first) {
e.preventDefault()
last.focus()
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault()
first.focus()
}
}
}
const activeDescendant = filtered.length ? optionId(activeIndex) : undefined
return (
<section className="cmdpal-scope relative w-full overflow-hidden bg-gradient-to-b from-slate-50 via-slate-100 to-slate-200 px-6 py-20 text-slate-900 dark:from-slate-950 dark:via-slate-900 dark:to-slate-950 dark:text-slate-100 sm:px-8 sm:py-28">
<style>{`
@keyframes cmdpal-fade-up {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes cmdpal-ping {
0% { transform: scale(1); opacity: .55; }
70%, 100% { transform: scale(2.2); opacity: 0; }
}
@keyframes cmdpal-grid-drift {
from { background-position: 0 0; }
to { background-position: 44px 44px; }
}
@media (prefers-reduced-motion: reduce) {
.cmdpal-scope *, .cmdpal-scope *::before, .cmdpal-scope *::after {
animation-duration: .001ms !important;
animation-iteration-count: 1 !important;
transition-duration: .001ms !important;
}
}
`}</style>
{/* Ambient backdrop grid */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 opacity-[0.5] [animation:cmdpal-grid-drift_22s_linear_infinite] [background-image:linear-gradient(to_right,rgba(100,116,139,0.09)_1px,transparent_1px),linear-gradient(to_bottom,rgba(100,116,139,0.09)_1px,transparent_1px)] [background-size:44px_44px] dark:opacity-40"
/>
<div aria-hidden="true" className="pointer-events-none absolute left-1/2 top-0 h-72 w-[42rem] max-w-full -translate-x-1/2 rounded-full bg-indigo-400/20 blur-[90px] dark:bg-indigo-500/20" />
<div className="relative mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-300/70 bg-white/70 px-3 py-1 text-xs font-medium uppercase tracking-wider text-slate-500 backdrop-blur dark:border-slate-700/70 dark:bg-slate-900/60 dark:text-slate-400">
<span className="relative flex h-1.5 w-1.5">
<span className="absolute inline-flex h-full w-full rounded-full bg-emerald-500 [animation:cmdpal-ping_2s_cubic-bezier(0,0,0.2,1)_infinite]" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
</span>
Nebula Console
</span>
<h2 className="mt-6 text-balance text-3xl font-semibold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
Everything, one keystroke away
</h2>
<p className="mx-auto mt-3 max-w-md text-pretty text-sm leading-relaxed text-slate-600 dark:text-slate-400 sm:text-base">
Navigate, create, and ship without lifting your hands from the keyboard. Open the command palette and just start typing.
</p>
<button
ref={triggerRef}
type="button"
onClick={() => setOpen(true)}
aria-haspopup="dialog"
aria-expanded={open}
className="group mx-auto mt-8 flex w-full max-w-md items-center gap-3 rounded-xl border border-slate-300/80 bg-white/80 px-4 py-3 text-left shadow-sm backdrop-blur transition hover:border-indigo-400/70 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 dark:border-slate-700/80 dark:bg-slate-900/70 dark:hover:border-indigo-500/70 dark:focus-visible:ring-offset-slate-950"
>
<SearchIcon className="h-4 w-4 shrink-0 text-slate-400 transition group-hover:text-indigo-500" />
<span className="flex-1 text-sm text-slate-500 dark:text-slate-400">Search commands…</span>
<span className="flex items-center gap-1">
<Kbd>⌘</Kbd>
<Kbd>K</Kbd>
</span>
</button>
{/* Live region + toast */}
<div aria-live="polite" className="sr-only">
{toast ? `Ran command: ${toast}` : ""}
</div>
</div>
<AnimatePresence>
{toast && (
<motion.div
key="toast"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 12 }}
transition={{ duration: reduce ? 0.001 : 0.22 }}
className="fixed bottom-6 left-1/2 z-50 -translate-x-1/2"
>
<div className="flex items-center gap-2.5 rounded-xl border border-slate-800/10 bg-slate-900 px-4 py-2.5 text-sm text-white shadow-xl dark:border-white/10 dark:bg-white dark:text-slate-900">
<EnterIcon className="h-4 w-4 text-emerald-400 dark:text-emerald-600" />
<span className="font-medium">{toast}</span>
<span className="text-slate-400 dark:text-slate-500">executed</span>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Modal */}
<AnimatePresence>
{open && (
<motion.div
key="overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: reduce ? 0.001 : 0.18 }}
className="fixed inset-0 z-40 flex items-start justify-center bg-slate-950/50 px-4 pt-[12vh] backdrop-blur-sm"
onMouseDown={(e) => {
if (e.target === e.currentTarget) setOpen(false)
}}
>
<motion.div
ref={panelRef}
role="dialog"
aria-modal="true"
aria-label="Command palette"
onKeyDown={onPanelKeyDown}
initial={reduce ? { opacity: 0 } : { opacity: 0, y: -10, scale: 0.98 }}
animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0, scale: 1 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: -8, scale: 0.98 }}
transition={reduce ? { duration: 0.001 } : { type: "spring", stiffness: 420, damping: 32 }}
className="w-full max-w-xl overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-2xl ring-1 ring-black/5 dark:border-slate-800 dark:bg-slate-900 dark:ring-white/5"
>
{/* Search row */}
<div className="flex items-center gap-3 border-b border-slate-200 px-4 dark:border-slate-800">
<SearchIcon className="h-5 w-5 shrink-0 text-slate-400" />
<input
ref={inputRef}
id={inputId}
type="text"
role="combobox"
aria-expanded="true"
aria-controls={listId}
aria-autocomplete="list"
aria-activedescendant={activeDescendant}
aria-label="Search commands"
value={query}
onChange={(e) => {
setQuery(e.target.value)
setActiveIndex(0)
}}
onKeyDown={onInputKeyDown}
placeholder="Type a command or search…"
autoComplete="off"
spellCheck={false}
className="w-full bg-transparent py-4 text-[15px] text-slate-900 outline-none placeholder:text-slate-400 dark:text-slate-100 dark:placeholder:text-slate-500"
/>
<button
type="button"
onClick={() => setOpen(false)}
aria-label="Close command palette"
className="shrink-0 rounded-md p-1.5 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:hover:bg-slate-800 dark:hover:text-slate-200"
>
<CloseIcon className="h-4 w-4" />
</button>
</div>
{/* Results */}
<div
id={listId}
role="listbox"
aria-label="Commands"
className="max-h-[min(56vh,26rem)] overflow-y-auto overscroll-contain p-2"
>
{filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-2 px-6 py-14 text-center [animation:cmdpal-fade-up_.3s_ease]">
<div className="flex h-11 w-11 items-center justify-center rounded-full bg-slate-100 text-slate-400 dark:bg-slate-800">
<SearchIcon className="h-5 w-5" />
</div>
<p className="text-sm font-medium text-slate-700 dark:text-slate-200">No matching commands</p>
<p className="max-w-[16rem] text-xs text-slate-500 dark:text-slate-400">
Nothing found for “<span className="font-medium text-slate-700 dark:text-slate-300">{query}</span>”. Try another keyword.
</p>
</div>
) : (
grouped.map((group) => (
<div key={group.section} role="group" aria-label={group.section} className="mb-1 last:mb-0">
<div className="px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500">
{group.section}
</div>
{group.items.map(({ cmd, index }) => {
const active = index === activeIndex
return (
<div
key={cmd.id}
id={optionId(index)}
role="option"
aria-selected={active}
tabIndex={-1}
onMouseMove={() => setActiveIndex(index)}
onClick={() => runCommand(cmd)}
className={[
"flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-colors",
active
? "bg-indigo-50 text-indigo-950 dark:bg-indigo-500/15 dark:text-indigo-100"
: "text-slate-700 dark:text-slate-200",
].join(" ")}
>
<span
className={[
"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border transition-colors",
active
? "border-indigo-300 bg-white text-indigo-600 dark:border-indigo-400/40 dark:bg-indigo-500/20 dark:text-indigo-200"
: "border-slate-200 bg-slate-50 text-slate-500 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400",
].join(" ")}
>
{cmd.icon}
</span>
<span className="flex min-w-0 flex-col">
<span className="truncate font-medium">{highlight(cmd.label, query)}</span>
<span className="truncate text-xs text-slate-500 dark:text-slate-400">
{highlight(cmd.hint, query)}
</span>
</span>
{cmd.shortcut && (
<span className="ml-auto flex shrink-0 items-center gap-1">
{cmd.shortcut.map((k, i) => (
<Kbd key={i}>{k}</Kbd>
))}
</span>
)}
{active && (
<span className="ml-1 shrink-0 text-indigo-400 dark:text-indigo-300" aria-hidden="true">
<EnterIcon className="h-4 w-4" />
</span>
)}
</div>
)
})}
</div>
))
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between gap-3 border-t border-slate-200 bg-slate-50/80 px-4 py-2.5 text-[11px] text-slate-500 dark:border-slate-800 dark:bg-slate-950/40 dark:text-slate-400">
<span className="flex items-center gap-3">
<span className="flex items-center gap-1.5">
<Kbd>↑</Kbd>
<Kbd>↓</Kbd>
<span>navigate</span>
</span>
<span className="flex items-center gap-1.5">
<Kbd>↵</Kbd>
<span>select</span>
</span>
<span className="hidden items-center gap-1.5 sm:flex">
<Kbd>esc</Kbd>
<span>close</span>
</span>
</span>
<span className="tabular-nums">
{filtered.length} {filtered.length === 1 ? "result" : "results"}
</span>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</section>
)
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Basic Modal
Originalbasic centred modal with overlay

Center Modal
Originalcentred modal with icon and actions

Form Modal
Originalmodal containing a form

Confirm Modal
Originalconfirm/destructive dialog

Alert Modal
Originalalert dialog with single action

Drawer Bottom Modal
Originalbottom sheet drawer

Side Drawer Modal
Originalright side drawer modal

Fullscreen Modal
Originalfullscreen modal

Image Lightbox Modal
Originalimage lightbox modal with nav

Video Modal
Originalvideo player modal

Scroll Modal
Originalmodal with long scrollable body

Steps Modal
Originalmulti-step modal flow

