CentercodeDitto
DittoPorygonAPI

CommandSearch

Global command palette for site-wide search and quick actions

Overview

CommandSearch provides a global command palette (inspired by VS Code, Linear, and Notion) for keyboard-driven navigation and actions. It supports static items for instant results and async search for dynamic content.

Key Features

  • Global keyboard shortcut (\u2318K / Ctrl+K) works from anywhere
  • Static items for instant results (navigation, actions)
  • Async search callback for dynamic API-based results
  • Grouped results with priorities and badges
  • Full keyboard navigation (arrows, enter, escape)

Interactive Demo

Click the trigger or press \u2318K to open the command palette. Try typing to search, or use arrow keys to navigate.

or

The command palette is controlled by the useCommandSearch hook. It can be opened programmatically or via keyboard shortcut.

LeftNav Integration

CommandSearch is designed to integrate with LayoutShell. The trigger appears in the LeftNav footer (above the collapse button) and is hidden when the sidebar is collapsed. The keyboard shortcut still works when collapsed.

// LayoutShell enables CommandSearch by default
<LayoutShell
  commandSearch={true}  // or custom config
>
  {children}
</LayoutShell>

// Custom configuration
<LayoutShell
  commandSearch={{
    items: myStaticItems,
    onSearch: async (query) => fetchResults(query),
    placeholder: "Search everything...",
  }}
>
  {children}
</LayoutShell>

Props Reference

CommandSearchProps

PropTypeDefaultDescription
itemsCommandSearchGroup[]-Static items always shown (navigation, actions)
onSearch(query: string) => Promise<CommandSearchResult | null>-Async search callback for dynamic results
placeholderstring-Search input placeholder text
emptyMessagestring-Message shown when no results found
openboolean-Controlled open state
onOpenChange(open: boolean) => void-Callback when open state changes
disableShortcutbooleanfalseDisable the global ⌘K keyboard shortcut

CommandSearchItem

PropTypeDefaultDescription
idstringrequiredUnique identifier
labelstringrequiredDisplay label
descriptionstring-Optional description shown below label
iconIconDefinition-Font Awesome icon
hrefstring-Navigation URL (mutually exclusive with action)
action() => void-Click handler (mutually exclusive with href)
shortcutstring-Keyboard shortcut display (e.g., "⌘N")
badgestring-Optional badge text
keywordsstring[]-Additional search keywords (not displayed)

Usage

import {
  CommandSearch,
  CommandSearchTrigger,
  useCommandSearch,
  type CommandSearchGroup,
} from '@/ui/components';

// Define static items
const items: CommandSearchGroup[] = [
  {
    id: 'navigation',
    label: 'Navigation',
    priority: 100,
    items: [
      { id: 'home', label: 'Home', icon: faHome, href: '/' },
      { id: 'projects', label: 'Projects', icon: faFolder, href: '/projects' },
    ],
  },
  {
    id: 'actions',
    label: 'Quick Actions',
    items: [
      {
        id: 'new-project',
        label: 'New Project',
        icon: faPlus,
        shortcut: '⌘N',
        action: () => router.push('/projects/new'),
      },
    ],
  },
];

// Use with hook for controlled state
function MyComponent() {
  const commandSearch = useCommandSearch();

  return (
    <>
      <CommandSearchTrigger onClick={commandSearch.open} />
      <CommandSearch
        items={items}
        onSearch={async (query) => {
          const results = await api.search(query);
          return { groups: [{ id: 'results', label: 'Results', items: results }] };
        }}
        open={commandSearch.isOpen}
        onOpenChange={commandSearch.setIsOpen}
      />
    </>
  );
}

Keyboard Navigation

\u2318K / Ctrl+KOpen command palette
TypeFilter results
\u2191 \u2193Navigate items
EnterSelect item
EscapeClose palette