Components
Loading preview...
a fast, accessible, mobile-friendly command/search palette with async multi-sources, fuzzy highlighting, recents, presets, pinned actions, and keyboard shortcuts.
@Scottclayton3d
npx shadcn@latest add https://21st.dev/r/lovesickfromthe6ix/omni-command-palette"use client";
import * as React from "react";
import { File, User, Cog, Rocket, Home, Link } from "lucide-react";
import { OmniCommandPalette, OmniSource, OmniItem } from "@/components/ui/omni-command-palette";
function wait(ms: number) {
return new Promise(res => setTimeout(res, ms));
}
const commandsSource: OmniSource = {
id: "commands",
label: "Commands",
async fetch(query: string) {
await wait(120);
const base: OmniItem[] = [
{
id: "new-project",
label: "Create project",
subtitle: "Spin up a new workspace",
groupId: "commands",
icon: <Rocket className="size-4" />,
pinned: true,
onAction: () => alert("Project created!"),
keywords: ["start", "init", "workspace"],
},
{
id: "settings",
label: "Open settings",
subtitle: "Profile, notifications, billing",
groupId: "commands",
icon: <Cog className="size-4" />,
shortcut: ["⌘", ","],
onAction: () => alert("Opening settings…"),
keywords: ["preferences"],
},
{
id: "home",
label: "Go to dashboard",
groupId: "commands",
icon: <Home className="size-4" />,
href: "/",
keywords: ["start", "main", "root"],
},
];
return filterByQuery(base, query);
},
};
const pagesSource: OmniSource = {
id: "pages",
label: "Pages",
async fetch(query: string) {
await wait(180);
const base: OmniItem[] = Array.from({ length: 12 }).map((_, i) => ({
id: `doc-${i + 1}`,
label: `Getting Started ${i + 1}`,
subtitle: "Documentation",
groupId: "pages",
icon: <File className="size-4" />,
href: `/docs/getting-started/${i + 1}`,
keywords: ["docs", "guide"],
}));
return filterByQuery(base, query);
},
};
const peopleSource: OmniSource = {
id: "people",
label: "People",
async fetch(query: string) {
await wait(140);
const base: OmniItem[] = [
{ id: "u-1", label: "Ava Williams", groupId: "people", icon: <User className="size-4" /> },
{ id: "u-2", label: "Liam Johnson", groupId: "people", icon: <User className="size-4" /> },
{ id: "u-3", label: "Sophia Chen", groupId: "people", icon: <User className="size-4" /> },
{ id: "u-4", label: "Noah Garcia", groupId: "people", icon: <User className="size-4" /> },
];
return filterByQuery(base, query);
},
};
function filterByQuery(items: OmniItem[], query: string) {
const q = query.trim().toLowerCase();
if (!q) return items;
return items.filter(i => i.label.toLowerCase().includes(q) || i.keywords?.some(k => k.toLowerCase().includes(q)));
}
export default function Demo() {
const [open, setOpen] = React.useState(false);
React.useEffect(() => {
const onDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
setOpen(v => !v);
}
};
window.addEventListener("keydown", onDown);
return () => window.removeEventListener("keydown", onDown);
}, []);
return (
<div className="relative min-h-[320px] w-full rounded-xl border bg-[hsl(var(--card))] p-6 text-[hsl(var(--foreground))]">
<h2 className="mb-2 text-lg font-semibold">Omni Command Palette</h2>
<p className="mb-4 text-sm text-[hsl(var(--muted-foreground))]">
Press <kbd className="rounded bg-[hsl(var(--muted))] px-1">⌘</kbd> + <kbd className="rounded bg-[hsl(var(--muted))] px-1">K</kbd> or click the button.
</p>
<button
onClick={() => setOpen(true)}
className="inline-flex items-center gap-2 rounded-lg border bg-[hsl(var(--accent))]/20 px-3 py-1.5 text-sm hover:bg-[hsl(var(--accent))]/30"
>
<Link className="size-4" />
Open Command Menu
</button>
<OmniCommandPalette
open={open}
onOpenChange={setOpen}
sources={[commandsSource, pagesSource, peopleSource]}
storageKey="demo:omni:recents"
showRecents
showPinnedFirst
onItemExecuted={(item) => console.log("Executed:", item)}
/>
</div>
);
}