Palette
moco/paletteFreeComponent
Cmd-K fuzzy command palette with subsequence matching, character highlighting, focus trap, and on-page section jumps.
Live preview
live · rendered by the registry
Rendered by moco on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://mocoui.site/r/palette.jsonSource
1"use client";23import * as React from "react";4import "./palette.css";5import { smoothScrollTo } from "@/components/moco/hooks";67export interface IndexEntry {8 kind: "post" | "page" | "section";9 label: string;10 href?: string;11 id?: string;12}1314export interface PaletteProps {15 index: IndexEntry[];16 /** Headings scraped into the index each time the palette opens. */17 sectionSelector?: string;18 /** Called for entries with an href. Defaults to a full navigation. */19 onNavigate?: (href: string) => void;20}2122/** Subsequence match. Returns matched character indices, or null. */23function fuzzy(needle: string, hay: string): number[] | null {24 if (!needle) return [];25 const h = hay.toLowerCase();26 const n = needle.toLowerCase();27 const hits: number[] = [];28 let i = 0;29 for (const ch of n) {30 const at = h.indexOf(ch, i);31 if (at === -1) return null;32 hits.push(at);33 i = at + 1;34 }35 return hits;36}3738function Highlighted({ text, hits }: { text: string; hits: number[] }) {39 const set = new Set(hits);40 return (41 <span>42 {text.split("").map((c, i) => (set.has(i) ? <mark key={i}>{c}</mark> : <span key={i}>{c}</span>))}43 </span>44 );45}4647/** ⌘K / Ctrl-K. Traps focus while open, restores it on close. */48export function Palette({49 index,50 sectionSelector = ".prose h2[id]",51 onNavigate,52}: PaletteProps) {53 const [open, setOpen] = React.useState(false);54 const [q, setQ] = React.useState("");55 const [sel, setSel] = React.useState(0);56 const [sections, setSections] = React.useState<IndexEntry[]>([]);57 const restore = React.useRef<HTMLElement | null>(null);58 const input = React.useRef<HTMLInputElement>(null);59 const box = React.useRef<HTMLDivElement>(null);6061 React.useEffect(() => {62 const onKey = (e: KeyboardEvent) => {63 if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {64 e.preventDefault();65 setOpen((v) => !v);66 }67 };68 document.addEventListener("keydown", onKey);69 return () => document.removeEventListener("keydown", onKey);70 }, []);7172 React.useEffect(() => {73 if (!open) {74 restore.current?.focus();75 return;76 }77 restore.current = document.activeElement as HTMLElement;78 setQ("");79 setSel(0);80 setSections(81 Array.from(document.querySelectorAll<HTMLElement>(sectionSelector)).map((h) => ({82 kind: "section" as const,83 label: h.textContent ?? "",84 id: h.id,85 })),86 );87// … truncated
What it pulls in
Other registry items
Files it writes
More from moco
All 20 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Codecode | moco | Components | Free | 1 dep · 3 files | |
| Comparecompare | moco | Components | Free | no deps · 2 files | |
| Count Upcountup | moco | Components | Free | no deps · 2 files | |
| Covercover | moco | Components | Free | no deps · 2 files | |
| Diagram Kitdiagram | moco | Components | Free | no deps · 2 files | |
| Dot Griddotgrid | moco | Components | Free | no deps · 2 files | |
| Figurefigure | moco | Components | Free | no deps · 2 files | |
| Hookshooks | moco | Components | Free | no deps |
