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
| Prop | Type | Default | Description |
|---|---|---|---|
items | CommandSearchGroup[] | - | Static items always shown (navigation, actions) |
onSearch | (query: string) => Promise<CommandSearchResult | null> | - | Async search callback for dynamic results |
placeholder | string | - | Search input placeholder text |
emptyMessage | string | - | Message shown when no results found |
open | boolean | - | Controlled open state |
onOpenChange | (open: boolean) => void | - | Callback when open state changes |
disableShortcut | boolean | false | Disable the global ⌘K keyboard shortcut |
CommandSearchItem
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique identifier |
label | string | required | Display label |
description | string | - | Optional description shown below label |
icon | IconDefinition | - | Font Awesome icon |
href | string | - | Navigation URL (mutually exclusive with action) |
action | () => void | - | Click handler (mutually exclusive with href) |
shortcut | string | - | Keyboard shortcut display (e.g., "⌘N") |
badge | string | - | Optional badge text |
keywords | string[] | - | 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