Innovations
modals /

Command Palette (⌘K)

Keyboard-driven command palette with grouped results, shortcuts, and a ⌘K trigger. Built with cmdk + Radix Dialog.

Preview

Source

tsx
"use client";

import { useState, useEffect, useCallback } from "react";
import { Command } from "cmdk";
import {
  Dialog,
  DialogContent,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import {
  Search,
  Home,
  FileText,
  BarChart2,
  Settings,
  Users,
  Mail,
  Plus,
  ExternalLink,
} from "lucide-react";

const actions = [
  { group: "Pages", icon: Home, label: "Go to Dashboard", shortcut: "G D" },
  { group: "Pages", icon: FileText, label: "View case studies", shortcut: "G C" },
  { group: "Pages", icon: BarChart2, label: "Open analytics", shortcut: "G A" },
  { group: "Actions", icon: Plus, label: "Create new project", shortcut: "N P" },
  { group: "Actions", icon: Mail, label: "Send email report", shortcut: "" },
  { group: "Actions", icon: Users, label: "Invite team member", shortcut: "" },
  { group: "Settings", icon: Settings, label: "Account settings", shortcut: "" },
  { group: "Settings", icon: ExternalLink, label: "Open documentation", shortcut: "" },
];

const groups = ["Pages", "Actions", "Settings"] as const;

export default function CommandPalette() {
  const [open, setOpen] = useState(false);

  const toggle = useCallback(() => setOpen((v) => !v), []);

  useEffect(() => {
    function onKeyDown(e: KeyboardEvent) {
      if ((e.metaKey || e.ctrlKey) && e.key === "k") {
        e.preventDefault();
        toggle();
      }
    }
    document.addEventListener("keydown", onKeyDown);
    return () => document.removeEventListener("keydown", onKeyDown);
  }, [toggle]);

  return (
    <div className="flex min-h-[200px] flex-col items-center justify-center gap-4 p-8">
      <Button
        variant="outline"
        onClick={toggle}
        className="w-full max-w-xs justify-between text-muted-foreground"
      >
        <span className="flex items-center gap-2">
          <Search className="h-4 w-4" />
          Search or jump to…
        </span>
        <kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border border-border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
          <span className="text-xs">⌘</span>K
        </kbd>
      </Button>
      <p className="text-xs text-muted-foreground">
        Or press <kbd className="font-mono text-foreground">⌘K</kbd> anywhere
      </p>

      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent className="overflow-hidden p-0 shadow-2xl sm:max-w-lg">
          <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
            <div className="flex items-center border-b border-border px-3">
              <Search className="mr-2 h-4 w-4 shrink-0 text-muted-foreground" />
              <Command.Input
                placeholder="Search actions, pages, settings…"
                className="flex h-12 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
              />
            </div>

            <Command.List className="max-h-80 overflow-y-auto overflow-x-hidden p-2">
              <Command.Empty className="py-8 text-center text-sm text-muted-foreground">
                No results found.
              </Command.Empty>

              {groups.map((group) => {
                const items = actions.filter((a) => a.group === group);
                return (
                  <Command.Group
                    key={group}
                    heading={group}
                    className="[&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-wide [&_[cmdk-group-heading]]:text-muted-foreground"
                  >
                    {items.map(({ icon: Icon, label, shortcut }) => (
                      <Command.Item
                        key={label}
                        value={label}
                        onSelect={() => setOpen(false)}
                        className="flex cursor-pointer items-center justify-between gap-2 rounded-lg px-2 py-2 text-sm text-foreground aria-selected:bg-muted"
                      >
                        <span className="flex items-center gap-2">
                          <Icon className="h-4 w-4 text-muted-foreground" />
                          {label}
                        </span>
                        {shortcut && (
                          <kbd className="pointer-events-none ml-auto inline-flex h-5 select-none items-center gap-1 rounded border border-border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
                            {shortcut}
                          </kbd>
                        )}
                      </Command.Item>
                    ))}
                  </Command.Group>
                );
              })}
            </Command.List>
          </Command>
        </DialogContent>
      </Dialog>
    </div>
  );
}
Claude Code Instructions

CLI Install

npx innovations add command-palette

Where to use it

Add this to your root layout so ⌘K opens globally on every page. In Next.js root layout (app/layout.tsx): import CommandPalette from '@/components/innovations/modals/command-palette'; // Render inside <body> at the end In Astro root layout: import CommandPalette from '../components/innovations/modals/command-palette'; <CommandPalette client:load /> The useEffect keydown listener is SSR-guarded (runs only in the browser). Extend the actions array with your real routes, features, and settings.