@glyx-dev/command
A keyboard-driven command palette with fuzzy search. Open it with any key combination, register commands from any component in the tree, and let users search across all of them.
Install
npm install @glyx-dev/commandHow it works
There are two parts:
-
useCommands(commands)— registers a list of commands into a global in-memory registry. Call it from any component. Commands are automatically removed when the component unmounts. -
<CommandPalette />— renders the palette UI. Render it once near the app root. It is invisible until triggered by the accelerator key.
When the user presses the accelerator, the palette opens over the current screen with a
search input. As the user types, results are ranked by fuzzy match score across the command's
label, section, and keywords. Pressing a result (or Enter) runs its action and closes
the palette. Pressing Escape closes it without acting.
Quick start
import { CommandPalette, useCommands } from '@glyx-dev/command'
export function App() {
return (
<View style={{ flex: 1 }}>
<MainContent />
<CommandPalette /> {/* render once — invisible until triggered */}
</View>
)
}
function MainContent() {
useCommands([
{ id: 'new-note', label: 'New Note', section: 'Notes', action: createNote },
{ id: 'search', label: 'Search Notes', section: 'Notes', action: openSearch, keywords: ['find', 'filter'] },
{ id: 'export-pdf', label: 'Export to PDF', section: 'File', action: exportPdf },
{ id: 'settings', label: 'Settings', section: 'App', action: openSettings },
{ id: 'dark-mode', label: 'Toggle Dark Mode', section: 'App', action: toggleTheme, keywords: ['theme', 'light'] },
])
return <NoteEditor />
}Default trigger: Ctrl+K on Windows/Linux, Cmd+K on macOS.
useCommands(commands)
Register commands from any component. The registry is global — commands from different components are all searched together.
useCommands([
{
id: string, // unique — used for deduplication
label: string, // shown in the palette
action: () => void,
section?: string, // group label shown left of the result (e.g. "File", "Edit", "View")
keywords?: string[], // extra search tokens not shown in the UI
}
])Registration is automatic. Commands are added on mount, removed on unmount.
You can call useCommands in as many components as you want — a note editor can register
editing commands, a sidebar can register navigation commands, and they all show up together.
Dynamic commands work naturally with React state:
function NoteEditor({ canSave }) {
useCommands([
{ id: 'save', label: canSave ? 'Save Note' : 'Save Note (no changes)', action: save },
])
// ...
}useCommands has an empty deps array — the command list is captured at mount time. If
your action closes over state that changes, use a ref to hold the latest value so the
callback always sees current state:
const noteRef = useRef(note)
useEffect(() => { noteRef.current = note }, [note])
useCommands([{ id: 'copy-id', label: 'Copy Note ID', action: () => clipboard.writeText(noteRef.current.id) }])<CommandPalette />
Render once near the root of your app. It renders nothing until opened.
<CommandPalette
accelerator="ctrl+k" // default — any shortcut string works
placeholder="Type a command…" // default
maxResults={8} // default
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
accelerator | string | 'ctrl+k' | Key combo to open/close the palette. See accelerator format. |
placeholder | string | 'Type a command…' | Search input placeholder text. |
maxResults | number | 8 | Maximum results shown at once. Results scroll if needed. |
Accelerator format
Modifiers joined with +, key last. Case-insensitive.
"ctrl+k" → Ctrl + K
"ctrl+shift+p" → Ctrl + Shift + P
"meta+k" → Cmd+K (macOS) / Win+K (Windows)
"ctrl+shift+/" → Ctrl + Shift + /Supported modifiers: ctrl (also control), shift, alt, meta (also cmd, win).
To match VS Code convention on all platforms:
import { input } from '@glyx-dev/react'
const isMac = input.platform?.() === 'macos'
<CommandPalette accelerator={isMac ? 'meta+k' : 'ctrl+k'} />Fuzzy search
Matching runs against the combined text of label + section + keywords:
- Exact substring: score 1.0 — always ranked first
- Subsequence: characters appear in order but not adjacent — ranked by density
- No match: entry hidden
keywords are extra tokens that don't show in the UI but boost discoverability:
{ id: 'dark', label: 'Toggle Dark Mode', keywords: ['theme', 'light', 'appearance', 'contrast'] }A user typing "light" or "appearance" finds "Toggle Dark Mode" even though neither word appears in the label.
Sections
section appears as a small label to the left of each result. Use it to group commands
by area of the app so the palette stays readable as the command list grows:
useCommands([
{ id: 'new', label: 'New Note', section: 'Notes' },
{ id: 'archive', label: 'Archive', section: 'Notes' },
{ id: 'import', label: 'Import File', section: 'File' },
{ id: 'export', label: 'Export PDF', section: 'File' },
{ id: 'prefs', label: 'Preferences', section: 'App' },
])Section names are displayed in uppercase.
Full example — multi-screen app
import { CommandPalette, useCommands } from '@glyx-dev/command'
import { useRoute, useNavigate } from '@glyx-dev/router'
import { clipboard } from '@glyx-dev/react'
// Root — palette lives here, always mounted
export function App() {
return (
<View style={{ flex: 1 }}>
<Router />
<CommandPalette accelerator="ctrl+k" />
</View>
)
}
// Each screen registers its own commands
function NotesScreen() {
const navigate = useNavigate()
const [notes, setNotes] = useState([])
const latestNotes = useRef(notes)
useEffect(() => { latestNotes.current = notes }, [notes])
useCommands([
{ id: 'notes-new', label: 'New Note', section: 'Notes', action: () => createNote() },
{ id: 'notes-search', label: 'Search Notes', section: 'Notes', action: () => navigate('search'), keywords: ['find'] },
{ id: 'notes-export', label: 'Export All', section: 'Notes', action: () => exportAll(latestNotes.current) },
{ id: 'go-settings', label: 'Settings', section: 'Go', action: () => navigate('settings') },
])
return <NoteList notes={notes} />
}
function SettingsScreen() {
useCommands([
{ id: 'settings-dark', label: 'Toggle Dark Mode', section: 'Settings', action: toggleTheme },
{ id: 'settings-reset', label: 'Reset Preferences', section: 'Settings', action: resetPrefs, keywords: ['clear', 'default'] },
{ id: 'go-notes', label: 'Go to Notes', section: 'Go', action: () => navigate('notes') },
])
// ...
}When the user navigates to a screen, its commands mount. When they leave, they unmount. The "Go to" cross-navigation commands registered by each screen help users jump anywhere without needing a mouse.
Styling
The palette uses Glyx's design system colors internally. Custom styling is not exposed in this release — file an issue if you need it.
API reference
| Export | Type | Description |
|---|---|---|
useCommands(commands) | Hook | Register commands into the global palette registry |
CommandPalette | Component | Palette UI — render once near the app root |