Skip to content

Command Palette

A single-level Cmd+K palette: a glass panel with fuzzy search and lit match letters, grouped results with a Recent section, a spring highlight that slides between rows, shortcut chips and a full keyboard model.

DOM + CSS0 deps1 file · 32 KB TSXreduced-motion aware

Freeminimalglass
Preview

Props

No runtime dependencies
"use client";

import {
  useCallback,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
  useSyncExternalStore,
} from "react";
import type { CSSProperties, KeyboardEvent as ReactKeyboardEvent } from "react";

export type CommandIcon =
  | "arrow"
  | "search"
  | "plus"
  | "copy"
  | "moon"
  | "sparkles"
  | "home"
  | "settings"
  | "user"
  | "link"
  | "doc"
  | "command"
  | "download"
  | "logout";

export type CommandItem = {
  id?: string;
  label: string;
  group?: string;
  icon?: CommandIcon;
  /** Keys as an array ["⌘", "N"] or a space separated string "⌘ N". */
  shortcut?: string | string[];
  /** Extra words the search also matches (not highlighted). */
  keywords?: string[];
  /** Shown first, under "Recent", until something else is run. */
  recent?: boolean;
  onSelect?: () => void;
};

const DEFAULT_ITEMS: CommandItem[] = [
  { label: "Create new component", group: "Create", icon: "plus", shortcut: ["⌘", "N"] },
  { label: "Search the library", group: "Navigate", icon: "search", shortcut: ["⌘", "/"], recent: true },
  { label: "Copy install command", group: "Actions", icon: "copy", shortcut: ["⌘", "⇧", "C"], recent: true },
  { label: "Toggle dark mode", group: "Preferences", icon: "moon", shortcut: ["⌘", "D"] },
  { label: "Open AI prompt", group: "Actions", icon: "sparkles", shortcut: ["⌘", "J"] },
];

const THEMES = {
  dark: {
    bg: "rgba(12,13,17,0.94)",
    border: "rgba(255,255,255,0.1)",
    text: "#f3f4f6",
    muted: "rgba(255,255,255,0.55)",
    faint: "rgba(255,255,255,0.34)",
    line: "rgba(255,255,255,0.07)",
    row: "rgba(255,255,255,0.065)",
    chip: "rgba(255,255,255,0.06)",
    chipLine: "rgba(255,255,255,0.11)",
    scrim: "rgba(2,3,5,0.52)",
    shadow: "0 40px 90px -24px rgba(0,0,0,0.85), 0 12px 30px -12px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.07)",
    mark: "var(--cp-accent)",
    glow: "rgba(255,255,255,0.055)",
  },
  light: {
    bg: "rgba(255,255,255,0.96)",
    border: "rgba(15,17,22,0.1)",
    text: "#0f1116",
    muted: "rgba(15,17,22,0.58)",
    faint: "rgba(15,17,22,0.4)",
    line: "rgba(15,17,22,0.08)",
    row: "rgba(15,17,22,0.055)",
    chip: "rgba(15,17,22,0.045)",
    chipLine: "rgba(15,17,22,0.13)",
    scrim: "rgba(15,17,22,0.3)",
    shadow: "0 40px 90px -28px rgba(15,17,22,0.35), 0 12px 30px -12px rgba(15,17,22,0.2), inset 0 1px 0 rgba(255,255,255,0.9)",
    mark: "color-mix(in srgb, var(--cp-accent) 72%, #000)",
    glow: "rgba(15,17,22,0.04)",
  },
} as const;

const CSS = [
  ".cmdp-scrim{animation:cmdp-fade .2s ease both}",
  ".cmdp-scrim[data-state=closing]{animation:cmdp-fade-out .16s ease both}",
  ".cmdp-pop{animation:cmdp-in .3s cubic-bezier(.16,1,.3,1) both}",
  ".cmdp-scrim[data-state=closing] .cmdp-pop{animation:cmdp-out .16s cubic-bezier(.4,0,1,1) both}",
  ".cmdp-row-in{animation:cmdp-row .32s cubic-bezier(.16,1,.3,1) both;animation-delay:calc(var(--i,0)*24ms)}",
  ".cmdp-list{transition:height .26s cubic-bezier(.2,.8,.2,1)}",
  ".cmdp-hi{transition:transform .3s cubic-bezier(.3,1.25,.4,1),height .22s ease,opacity .15s ease,background-color .14s ease}",
  ".cmdp-hi::before{content:'';position:absolute;left:0;top:50%;width:2px;height:14px;margin-top:-7px;border-radius:2px;background:var(--cp-accent);box-shadow:0 0 12px var(--cp-accent)}",
  ".cmdp-hi[data-picked]{background-color:color-mix(in srgb,var(--cp-accent) 26%,transparent)!important}",
  ".cmdp-head::after{content:'';position:absolute;left:0;right:0;bottom:-1px;height:1px;background:linear-gradient(90deg,transparent,var(--cp-accent),transparent);opacity:0;transition:opacity .25s ease}",
  ".cmdp-head:focus-within::after{opacity:.75}",
  ".cmdp-sweep{animation:cmdp-sweep .7s cubic-bezier(.3,.7,.2,1) both}",
  ".cmdp-glow{opacity:0;transition:opacity .3s ease}",
  ".cmdp-panel:hover .cmdp-glow{opacity:1}",
  "@keyframes cmdp-fade{from{opacity:0}to{opacity:1}}",
  "@keyframes cmdp-fade-out{to{opacity:0}}",
  "@keyframes cmdp-in{from{opacity:0;transform:translateY(-12px) scale(.96)}to{opacity:1;transform:none}}",
  "@keyframes cmdp-out{to{opacity:0;transform:translateY(-8px) scale(.98)}}",
  "@keyframes cmdp-row{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}",
  "@keyframes cmdp-sweep{0%{opacity:.9;transform:translateX(-100%)}100%{opacity:0;transform:translateX(100%)}}",
  "@media (prefers-reduced-motion:reduce){.cmdp-pop,.cmdp-row-in,.cmdp-sweep{animation:none!important}.cmdp-scrim,.cmdp-scrim[data-state=closing]{animation-duration:.01s!important}.cmdp-list,.cmdp-hi{transition:none!important}}",
].join("\n");

/* ---------- fuzzy search ---------- */

type Match = { score: number; idx: number[] };

function isBoundary(t: string, i: number) {
  return i === 0 || " -_/.".includes(t.charAt(i - 1));
}

function fuzzy(query: string, text: string): Match | null {
  const t = text.toLowerCase();
  const sub = t.indexOf(query);
  if (sub >= 0) {
    const idx = Array.from({ length: query.length }, (_, k) => sub + k);
    return { score: 1000 - sub * 4 + (isBoundary(t, sub) ? 220 : 0) - (t.length - query.length) * 0.6, idx };
  }
  const q = query.replace(/\s+/g, "");
  if (!q) return null;
  let best: Match | null = null;
  for (let s = t.indexOf(q.charAt(0)); s >= 0; s = t.indexOf(q.charAt(0), s + 1)) {
    const idx = [s];
    let score = isBoundary(t, s) ? 30 : 0;
    let prev = s;
    let ok = true;
    for (let k = 1; k < q.length; k++) {
      const j = t.indexOf(q.charAt(k), prev + 1);
      if (j < 0) {
        ok = false;
        break;
      }
      score += j === prev + 1 ? 18 : -(j - prev - 1) * 1.5;
      if (isBoundary(t, j)) score += 24;
      idx.push(j);
      prev = j;
    }
    if (!ok) continue;
    score -= s * 0.8;
    if (!best || score > best.score) best = { score, idx };
  }
  return best;
}

type Norm = {
  id: string;
  label: string;
  group: string;
  icon: CommandIcon;
  keys: string[];
  words: string;
  recent: boolean;
  onSelect?: () => void;
};
type Row = { item: Norm; idx: number[]; score: number };
type Section = { title: string; rows: Row[] };

function normalize(items: ReadonlyArray<string | CommandItem>): Norm[] {
  const seen = new Set<string>();
  return items.map((raw, n) => {
    const it: CommandItem = typeof raw === "string" ? { label: raw } : raw;
    let id = it.id ?? it.label;
    if (seen.has(id)) id = id + "#" + n;
    seen.add(id);
    const sc = it.shortcut;
    const keys = Array.isArray(sc) ? sc : sc ? (sc.includes(" ") ? sc.split(/\s+/) : Array.from(sc)) : [];
    return {
      id,
      label: it.label,
      group: it.group ?? "Commands",
      icon: it.icon ?? "arrow",
      keys,
      words: (it.keywords ?? []).join(" ").toLowerCase(),
      recent: !!it.recent,
      onSelect: it.onSelect,
    };
  });
}

function buildSections(items: Norm[], rawQuery: string, recentIds: string[]): Section[] {
  const query = rawQuery.trim().toLowerCase();
  if (!query) {
    const byId = new Map(items.map((i) => [i.id, i]));
    const recents = recentIds.map((id) => byId.get(id)).filter((i): i is Norm => !!i);
    const taken = new Set(recents.map((i) => i.id));
    const sections: Section[] = [];
    if (recents.length) sections.push({ title: "Recent", rows: recents.map((item) => ({ item, idx: [], score: 0 })) });
    const order: string[] = [];
    const map = new Map<string, Row[]>();
    for (const item of items) {
      if (taken.has(item.id)) continue;
      if (!map.has(item.group)) {
        map.set(item.group, []);
        order.push(item.group);
      }
      map.get(item.group)?.push({ item, idx: [], score: 0 });
    }
    for (const g of order) sections.push({ title: g, rows: map.get(g) ?? [] });
    return sections;
  }
  const map = new Map<string, Row[]>();
  for (const item of items) {
    const m = fuzzy(query, item.label);
    let row: Row | null = m ? { item, idx: m.idx, score: m.score } : null;
    if (!row && item.words) {
      const w = fuzzy(query, item.words);
      if (w) row = { item, idx: [], score: w.score - 400 };
    }
    if (!row) continue;
    if (!map.has(item.group)) map.set(item.group, []);
    map.get(item.group)?.push(row);
  }
  const sections: Section[] = [];
  for (const [title, rows] of map) {
    rows.sort((a, b) => b.score - a.score);
    sections.push({ title, rows });
  }
  sections.sort((a, b) => (b.rows[0]?.score ?? 0) - (a.rows[0]?.score ?? 0));
  return sections;
}

/* ---------- small pieces ---------- */

const ICON_PATHS: Record<CommandIcon, string[]> = {
  arrow: ["M5 12h14", "M13 6l6 6-6 6"],
  search: ["M20 20l-3.6-3.6"],
  plus: ["M12 5v14", "M5 12h14"],
  copy: ["M5 15V6a2 2 0 0 1 2-2h9"],
  moon: ["M20 14.5A8 8 0 0 1 9.5 4 8 8 0 1 0 20 14.5Z"],
  sparkles: ["M11 3l1.8 5.2L18 10l-5.2 1.8L11 17l-1.8-5.2L4 10l5.2-1.8L11 3Z", "M19 16v4", "M17 18h4"],
  home: ["M4 11l8-7 8 7v8a1 1 0 0 1-1 1h-4v-6h-6v6H5a1 1 0 0 1-1-1v-8Z"],
  settings: ["M4 7h9", "M19 7h1", "M4 17h1", "M11 17h9"],
  user: ["M4 20a8 8 0 0 1 16 0"],
  link: ["M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1", "M14 10a4 4 0 0 0-5.7 0l-3 3A4 4 0 0 0 11 18.7l1-1"],
  doc: ["M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z", "M14 3v5h5", "M9 13h6", "M9 17h6"],
  command: ["M9 9V6a3 3 0 1 0-3 3h3Zm0 0h6m-6 0v6m6-6V6a3 3 0 1 1 3 3h-3Zm0 0v6m0 0h3a3 3 0 1 1-3 3v-3Zm0 0H9m0 0v3a3 3 0 1 1-3-3h3Z"],
  download: ["M12 4v11", "M7 11l5 5 5-5", "M5 20h14"],
  logout: ["M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4", "M16 17l5-5-5-5", "M21 12H9"],
};

function Glyph({ name, size = 16 }: { name: CommandIcon; size?: number }) {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.7}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      focusable="false"
      className="shrink-0"
    >
      {name === "search" && <circle cx="11" cy="11" r="7" />}
      {name === "copy" && <rect x="9" y="9" width="11" height="11" rx="2" />}
      {name === "settings" && (
        <>
          <circle cx="16" cy="7" r="2.2" />
          <circle cx="8" cy="17" r="2.2" />
        </>
      )}
      {name === "user" && <circle cx="12" cy="8" r="4" />}
      {ICON_PATHS[name].map((d) => (
        <path key={d} d={d} />
      ))}
    </svg>
  );
}

function Kbd({ children, theme, mac }: { children: string; theme: (typeof THEMES)["dark"] | (typeof THEMES)["light"]; mac: boolean }) {
  return (
    <kbd
      className="inline-flex h-[20px] min-w-[20px] items-center justify-center rounded-[6px] border px-1 text-[11px] font-medium leading-none"
      style={{
        fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
        background: theme.chip,
        borderColor: theme.chipLine,
        color: theme.muted,
      }}
    >
      {!mac && children === "⌘" ? "Ctrl" : children}
    </kbd>
  );
}

function Label({ text, idx }: { text: string; idx: number[] }) {
  if (!idx.length) return <>{text}</>;
  const on = new Set(idx);
  const out: Array<{ s: string; m: boolean }> = [];
  for (let i = 0; i < text.length; i++) {
    const m = on.has(i);
    const last = out[out.length - 1];
    if (last && last.m === m) last.s += text.charAt(i);
    else out.push({ s: text.charAt(i), m });
  }
  return (
    <>
      {out.map((r, i) =>
        r.m ? (
          <span key={i} style={{ color: "var(--cp-mark)", fontWeight: 600 }}>
            {r.s}
          </span>
        ) : (
          <span key={i}>{r.s}</span>
        ),
      )}
    </>
  );
}

const subscribeNone = () => () => {};
const getIsMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
const getServerMac = () => true;
const LIST_CAP = 340;
const HEX = /^#[0-9a-f]{3,8}$/i;

type Phase = "closed" | "open" | "closing";

export function CommandPalette({
  items = DEFAULT_ITEMS,
  placeholder = "Type a command...",
  defaultOpen = false,
  hotkey = true,
  contained = false,
  theme = "dark",
  accent = "#7b93ff",
  triggerLabel = "Search commands",
  onSelect,
}: {
  items?: ReadonlyArray<string | CommandItem>;
  placeholder?: string;
  /** Mount with the palette already open (no autofocus). */
  defaultOpen?: boolean;
  /** true: global ⌘K / Ctrl+K. "local": only while the pointer or focus is on the trigger or palette. false: off. */
  hotkey?: boolean | "local";
  /** Anchor the overlay to the nearest positioned ancestor instead of the viewport. */
  contained?: boolean;
  theme?: "dark" | "light";
  accent?: string;
  triggerLabel?: string;
  onSelect?: (item: { id: string; label: string }) => void;
}) {
  const uid = useId();
  const listId = uid + "-list";
  const mac = useSyncExternalStore(subscribeNone, getIsMac, getServerMac);
  const t = THEMES[theme === "light" ? "light" : "dark"];
  const accentColor = HEX.test(accent) ? accent : "#7b93ff";

  const [phase, setPhase] = useState<Phase>(defaultOpen ? "open" : "closed");
  const [query, setQuery] = useState("");
  const [active, setActive] = useState(0);
  const [picked, setPicked] = useState(false);
  const [note, setNote] = useState("");
  const [recentIds, setRecentIds] = useState<string[]>(() =>
    normalize(items)
      .filter((i) => i.recent)
      .slice(0, 3)
      .map((i) => i.id),
  );

  const rootRef = useRef<HTMLSpanElement>(null);
  const triggerRef = useRef<HTMLButtonElement>(null);
  const panelRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);
  const listRef = useRef<HTMLDivElement>(null);
  const innerRef = useRef<HTMLDivElement>(null);
  const hiRef = useRef<HTMLDivElement>(null);
  const phaseRef = useRef<Phase>(phase);
  const focusOnOpen = useRef(false);
  const kbdNav = useRef(false);
  const placed = useRef(false);
  const pickTimer = useRef(0);
  const pendingRecent = useRef("");

  const norm = useMemo(() => normalize(items), [items]);
  const sections = useMemo(() => buildSections(norm, query, recentIds), [norm, query, recentIds]);
  const flat = useMemo(() => sections.flatMap((s) => s.rows), [sections]);
  const sig = useMemo(() => flat.map((r) => r.item.id).join("|"), [flat]);
  const activeIdx = Math.min(active, Math.max(flat.length - 1, 0));
  const activeRow = flat[activeIdx];
  const isOpen = phase !== "closed";
  const hasQuery = query.trim().length > 0;

  useEffect(() => {
    phaseRef.current = phase;
  }, [phase]);

  const openPalette = useCallback(() => {
    window.clearTimeout(pickTimer.current);
    focusOnOpen.current = true;
    const ran = pendingRecent.current;
    if (ran) {
      pendingRecent.current = "";
      setRecentIds((r) => [ran, ...r.filter((x) => x !== ran)].slice(0, 3));
    }
    setQuery("");
    setActive(0);
    setPicked(false);
    setPhase("open");
  }, []);

  const closePalette = useCallback(() => {
    window.clearTimeout(pickTimer.current);
    const within = panelRef.current?.contains(document.activeElement);
    setPhase("closing");
    if (within) triggerRef.current?.focus({ preventScroll: true });
  }, []);

  useEffect(() => {
    if (!hotkey) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key.toLowerCase() !== "k" || !(e.metaKey || e.ctrlKey) || e.altKey || e.shiftKey) return;
      if (hotkey === "local") {
        const root = rootRef.current;
        if (!root || !(root.contains(document.activeElement) || root.matches(":hover"))) return;
      }
      e.preventDefault();
      if (hotkey === "local") e.stopImmediatePropagation();
      if (phaseRef.current === "open") closePalette();
      else openPalette();
    };
    document.addEventListener("keydown", onKey, true);
    return () => document.removeEventListener("keydown", onKey, true);
  }, [hotkey, openPalette, closePalette]);

  useEffect(() => {
    if (phase !== "closing") return;
    const id = window.setTimeout(() => setPhase("closed"), 170);
    return () => window.clearTimeout(id);
  }, [phase]);

  useEffect(() => {
    if (phase === "open" && focusOnOpen.current) {
      focusOnOpen.current = false;
      inputRef.current?.focus({ preventScroll: true });
    }
  }, [phase]);

  useEffect(() => {
    if (contained || phase === "closed") return;
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.body.style.overflow = prev;
    };
  }, [contained, phase]);

  useEffect(() => () => window.clearTimeout(pickTimer.current), []);

  // Slide the highlight to the active row; keep it in view for keyboard travel.
  useEffect(() => {
    const hi = hiRef.current;
    const inner = innerRef.current;
    const list = listRef.current;
    if (!isOpen) {
      placed.current = false;
      return;
    }
    if (!hi || !inner || !list) return;
    const row = inner.querySelector<HTMLElement>('[data-idx="' + activeIdx + '"]');
    if (!row) {
      hi.style.opacity = "0";
      return;
    }
    hi.style.opacity = "1";
    hi.style.height = row.offsetHeight + "px";
    if (!placed.current) hi.style.transition = "none";
    hi.style.transform = "translateY(" + row.offsetTop + "px)";
    if (!placed.current) {
      void hi.offsetHeight;
      hi.style.transition = "";
      placed.current = true;
    }
    if (kbdNav.current) {
      kbdNav.current = false;
      const top = row.offsetTop - 8;
      const bottom = row.offsetTop + row.offsetHeight + 8;
      if (top < list.scrollTop) list.scrollTop = top;
      else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight;
    }
  }, [activeIdx, sig, isOpen]);

  // Ease the list height as results come and go.
  useEffect(() => {
    if (!isOpen) return;
    const list = listRef.current;
    const inner = innerRef.current;
    if (!list || !inner || typeof ResizeObserver === "undefined") return;
    const fit = () => {
      list.style.height = Math.min(inner.offsetHeight, LIST_CAP) + "px";
    };
    fit();
    const ro = new ResizeObserver(fit);
    ro.observe(inner);
    return () => ro.disconnect();
  }, [isOpen]);

  const run = (row: Row | undefined) => {
    if (!row || picked) return;
    const { item } = row;
    setPicked(true);
    setNote("Ran " + item.label);
    item.onSelect?.();
    onSelect?.({ id: item.id, label: item.label });
    window.clearTimeout(pickTimer.current);
    pickTimer.current = window.setTimeout(() => {
      setPicked(false);
      pendingRecent.current = item.id;
      closePalette();
    }, 150);
  };

  const move = (to: number) => {
    if (!flat.length) return;
    kbdNav.current = true;
    setActive(((to % flat.length) + flat.length) % flat.length);
  };

  const onKeyDown = (e: ReactKeyboardEvent) => {
    const k = e.key;
    if (k === "Escape") {
      e.preventDefault();
      closePalette();
    } else if (k === "ArrowDown" || (e.ctrlKey && (k === "n" || k === "j"))) {
      e.preventDefault();
      move(activeIdx + 1);
    } else if (k === "ArrowUp" || (e.ctrlKey && (k === "p" || k === "k"))) {
      e.preventDefault();
      move(activeIdx - 1);
    } else if (k === "PageDown") {
      e.preventDefault();
      move(Math.min(activeIdx + 5, flat.length - 1));
    } else if (k === "PageUp") {
      e.preventDefault();
      move(Math.max(activeIdx - 5, 0));
    } else if (k === "Enter") {
      if (e.nativeEvent.isComposing) return;
      e.preventDefault();
      run(activeRow);
    } else if (k === "Tab") {
      e.preventDefault();
    }
  };

  const onPanelMove = (e: React.PointerEvent<HTMLDivElement>) => {
    const el = e.currentTarget;
    const r = el.getBoundingClientRect();
    if (!r.width) return;
    const k = el.offsetWidth / r.width;
    el.style.setProperty("--mx", (e.clientX - r.left) * k + "px");
    el.style.setProperty("--my", (e.clientY - r.top) * k + "px");
  };

  const vars = {
    "--cp-accent": accentColor,
    "--cp-mark": t.mark,
  } as CSSProperties;

  let counter = 0;
  const total = flat.length;

  return (
    <span ref={rootRef} style={{ display: "contents" }}>
      <style>{CSS}</style>
      <button
        ref={triggerRef}
        type="button"
        onClick={openPalette}
        aria-haspopup="dialog"
        aria-expanded={isOpen}
        aria-keyshortcuts={hotkey ? "Meta+K Control+K" : undefined}
        className="inline-flex items-center gap-3 rounded-full border px-4 py-2.5 text-sm transition-[transform,background-color,border-color,opacity] duration-150 hover:opacity-100 active:scale-[0.97] focus-visible:!outline-2 focus-visible:!outline-offset-2 focus-visible:!outline-current motion-reduce:transition-none"
        style={{
          borderColor: "color-mix(in srgb, currentColor 24%, transparent)",
          background: "color-mix(in srgb, currentColor 4%, transparent)",
          opacity: 0.82,
        }}
      >
        <Glyph name="search" size={15} />
        {triggerLabel}
        <kbd
          className="rounded-md border px-1.5 py-0.5 text-[10px] leading-none"
          style={{
            fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
            borderColor: "color-mix(in srgb, currentColor 24%, transparent)",
            background: "color-mix(in srgb, currentColor 7%, transparent)",
          }}
        >
          {mac ? "⌘K" : "Ctrl K"}
        </kbd>
      </button>
      <span className="sr-only" role="status" aria-live="polite">
        {note}
      </span>

      {isOpen && (
        <div
          className="cmdp-scrim"
          data-state={phase}
          onPointerDown={(e) => {
            if (e.target === e.currentTarget) closePalette();
          }}
          style={{
            ...vars,
            position: contained ? "absolute" : "fixed",
            inset: 0,
            zIndex: contained ? 20 : 2147483000,
            display: "flex",
            justifyContent: "center",
            alignItems: "flex-start",
            padding: (contained ? "48px" : "clamp(16px, 14vh, 140px)") + " 12px 12px",
            background: t.scrim,
            backdropFilter: "blur(3px)",
            WebkitBackdropFilter: "blur(3px)",
            color: t.text,
            fontFamily: "inherit",
          }}
        >
          <div
            ref={panelRef}
            role="dialog"
            aria-modal="true"
            aria-label="Command palette"
            className="cmdp-pop cmdp-panel relative flex w-full flex-col overflow-hidden rounded-[18px] border"
            onKeyDown={onKeyDown}
            onPointerMove={onPanelMove}
            onPointerDown={(e) => {
              const el = e.target as HTMLElement;
              if (!el.closest("input")) {
                e.preventDefault();
                inputRef.current?.focus({ preventScroll: true });
              }
            }}
            style={{
              maxWidth: 560,
              maxHeight: "100%",
              background: t.bg,
              borderColor: t.border,
              boxShadow: t.shadow,
              backdropFilter: "blur(24px) saturate(1.4)",
              WebkitBackdropFilter: "blur(24px) saturate(1.4)",
            }}
          >
            <div
              aria-hidden="true"
              className="cmdp-glow pointer-events-none absolute inset-0 z-0"
              style={{ background: "radial-gradient(340px circle at var(--mx,50%) var(--my,0%), " + t.glow + ", transparent 70%)" }}
            />
            <div className="cmdp-head relative z-[1] flex shrink-0 items-center gap-3 border-b px-4" style={{ borderColor: t.line, height: 54 }}>
              <span style={{ color: t.faint }}>
                <Glyph name="search" size={18} />
              </span>
              <input
                ref={inputRef}
                value={query}
                onChange={(e) => {
                  setQuery(e.target.value);
                  setActive(0);
                  if (listRef.current) listRef.current.scrollTop = 0;
                }}
                placeholder={placeholder}
                role="combobox"
                aria-expanded="true"
                aria-controls={listId}
                aria-autocomplete="list"
                aria-activedescendant={activeRow ? uid + "-o-" + activeIdx : undefined}
                aria-label="Search commands"
                autoComplete="off"
                autoCorrect="off"
                autoCapitalize="off"
                spellCheck={false}
                enterKeyHint="go"
                className="cmdp-in h-full min-w-0 flex-1 bg-transparent text-[15px]"
                style={{ outline: "none", border: 0, color: t.text, caretColor: accentColor }}
              />
              <style>{".cmdp-in::placeholder{color:" + t.faint + "}"}</style>
              {hasQuery ? (
                <button
                  type="button"
                  tabIndex={-1}
                  aria-label="Clear search"
                  onClick={() => {
                    setQuery("");
                    setActive(0);
                    inputRef.current?.focus({ preventScroll: true });
                  }}
                  className="grid h-7 w-7 shrink-0 place-items-center rounded-full transition-colors"
                  style={{ background: t.chip, color: t.muted }}
                >
                  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" aria-hidden="true">
                    <path d="M6 6l12 12M18 6L6 18" />
                  </svg>
                </button>
              ) : (
                <button
                  type="button"
                  tabIndex={-1}
                  aria-label="Close command palette"
                  onClick={closePalette}
                  className="shrink-0 rounded-[6px] border px-1.5 py-1 text-[10px] font-medium uppercase leading-none tracking-wider"
                  style={{ borderColor: t.chipLine, background: t.chip, color: t.faint }}
                >
                  esc
                </button>
              )}
              <span
                key={query.length}
                aria-hidden="true"
                className={hasQuery ? "cmdp-sweep pointer-events-none absolute -bottom-px left-0 h-px w-full" : "hidden"}
                style={{ background: "linear-gradient(90deg, transparent, " + accentColor + ", transparent)" }}
              />
            </div>

            <div
              ref={listRef}
              id={listId}
              role="listbox"
              aria-label="Commands"
              className="cmdp-list relative z-[1] min-h-0 shrink overflow-y-auto overscroll-contain"
              style={{ maxHeight: LIST_CAP, scrollbarWidth: "thin", scrollbarColor: t.chipLine + " transparent" }}
            >
              <div ref={innerRef} className="relative" style={{ padding: 6 }}>
                <div
                  ref={hiRef}
                  aria-hidden="true"
                  className="cmdp-hi pointer-events-none absolute z-0 rounded-[11px]"
                  data-picked={picked ? "" : undefined}
                  style={{
                    left: 6,
                    right: 6,
                    top: 0,
                    height: 0,
                    opacity: 0,
                    background: t.row,
                    boxShadow: "inset 0 0 0 1px " + t.line,
                  }}
                />
                {total === 0 && (
                  <div className="relative z-[1] flex flex-col items-center gap-2 px-6 py-9 text-center">
                    <span
                      className="grid h-10 w-10 place-items-center rounded-xl border"
                      style={{ borderColor: t.chipLine, background: t.chip, color: t.faint }}
                    >
                      <Glyph name="search" size={18} />
                    </span>
                    <p className="text-sm font-medium" style={{ color: t.text }}>
                      No commands match “{query.trim()}”
                    </p>
                    <p className="text-xs" style={{ color: t.faint }}>
                      Check the spelling or try a shorter search.
                    </p>
                  </div>
                )}
                {sections.map((sec) => (
                  <div key={sec.title} role="group" aria-label={sec.title}>
                    <div className="px-2.5 pb-1 pt-2.5 text-[11px] font-medium tracking-[0.04em]" style={{ color: t.faint }} aria-hidden="true">
                      {sec.title}
                    </div>
                    {sec.rows.map((row) => {
                      const i = counter++;
                      const on = i === activeIdx;
                      return (
                        <div
                          key={row.item.id}
                          id={uid + "-o-" + i}
                          role="option"
                          aria-selected={on}
                          data-idx={i}
                          className="cmdp-row-in relative z-[1] flex min-h-[42px] cursor-pointer select-none items-center gap-3 rounded-[11px] px-2.5 py-1.5 text-[14px] transition-transform duration-100 active:scale-[0.985] motion-reduce:transition-none"
                          style={{ "--i": Math.min(i, 9), color: t.text } as CSSProperties}
                          onPointerMove={() => {
                            if (i !== activeIdx) setActive(i);
                          }}
                          onPointerDown={(e) => {
                            if (e.pointerType !== "mouse" && i !== activeIdx) setActive(i);
                          }}
                          onClick={() => run(row)}
                        >
                          <span
                            className="grid h-7 w-7 shrink-0 place-items-center rounded-[9px] border transition-colors duration-200"
                            style={{
                              borderColor: on ? "color-mix(in srgb, var(--cp-accent) 40%, transparent)" : t.chipLine,
                              background: on ? "color-mix(in srgb, var(--cp-accent) 16%, transparent)" : t.chip,
                              color: on ? "var(--cp-mark)" : t.muted,
                            }}
                          >
                            <Glyph name={row.item.icon} size={15} />
                          </span>
                          <span className="min-w-0 flex-1 truncate" style={{ color: on ? t.text : "color-mix(in srgb, " + t.text + " 88%, transparent)" }}>
                            <Label text={row.item.label} idx={row.idx} />
                          </span>
                          {row.item.keys.length > 0 ? (
                            <span className="flex shrink-0 items-center gap-1">
                              {row.item.keys.map((k, n) => (
                                <Kbd key={n} theme={t} mac={mac}>
                                  {k}
                                </Kbd>
                              ))}
                            </span>
                          ) : (
                            on && (
                              <span className="shrink-0">
                                <Kbd theme={t} mac={mac}>
                                  ↵
                                </Kbd>
                              </span>
                            )
                          )}
                        </div>
                      );
                    })}
                  </div>
                ))}
              </div>
            </div>

            <div
              className="relative z-[1] flex shrink-0 items-center justify-between gap-3 border-t px-4 text-[11px]"
              style={{ borderColor: t.line, height: 38, color: t.faint }}
            >
              <span className="flex items-center gap-3" aria-hidden="true">
                <span className="flex items-center gap-1.5">
                  <Kbd theme={t} mac={mac}>↑</Kbd>
                  <Kbd theme={t} mac={mac}>↓</Kbd>
                  <span>Move</span>
                </span>
                <span className="flex items-center gap-1.5">
                  <Kbd theme={t} mac={mac}>↵</Kbd>
                  <span>Run</span>
                </span>
              </span>
              <span aria-live="polite" role="status">
                {total} {hasQuery ? (total === 1 ? "result" : "results") : total === 1 ? "command" : "commands"}
              </span>
            </div>
          </div>
        </div>
      )}
    </span>
  );
}

Free. Pro unlocks everything, from $27/month.

Details

About this pattern

A single-level Cmd+K palette: one trigger pill and one panel, no nested pages. Typing runs a subsequence fuzzy match, scored for contiguous runs and word starts, and the matched letters light up in the accent colour. With an empty query results are grouped, and anything you have run climbs into a Recent group at the top (up to three). Items are plain strings or objects with a group, icon, shortcut chips, hidden keywords and an onSelect callback. A single highlight element slides between rows on a springy curve, the list height eases as results come and go, and the panel tracks the pointer with a soft light. The shortcut is wired: Cmd+K or Ctrl+K toggles the palette globally, or hotkey set to “local” limits it to when the pointer or focus is on it. Everything is React and Tailwind with inline SVG, no framer-motion or icon library. The panel paints its own surface and has a dark and a light theme; the trigger inherits the page text colour so it sits on light or dark sites.

Curator’s note

Rebuilt in October as a dependency-free palette with real fuzzy search, a sliding highlight and a complete keyboard model. The preview opens it inside its own stage (the contained prop); in your app leave that off and it overlays the page with fixed positioning.

Pairs well with

Pro