🚧 Glyx is pre-release software. APIs may change before v1.0. Get started →
Documentation
Packages
@glyx-dev/command

@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/command

How it works

There are two parts:

  1. 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.

  2. <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

PropTypeDefaultDescription
acceleratorstring'ctrl+k'Key combo to open/close the palette. See accelerator format.
placeholderstring'Type a command…'Search input placeholder text.
maxResultsnumber8Maximum 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

ExportTypeDescription
useCommands(commands)HookRegister commands into the global palette registry
CommandPaletteComponentPalette UI — render once near the app root