Guide
Getting started
⌘K for your app, in one call.
npm install @devix-labs/command-palette
import { createCommandPalette } from '@devix-labs/command-palette';
import '@devix-labs/command-palette/styles.css';
const palette = createCommandPalette({
items: [
{ id: 'invoice.new', title: 'New invoice', section: 'Create', shortcut: 'mod+shift+n', perform: () => newInvoice() },
{ id: 'go.settings', title: 'Open settings', section: 'Go to', subtitle: 'Workspace preferences', perform: () => router.push('/settings') },
{ id: 'go.billing', title: 'Open billing', section: 'Go to', perform: () => router.push('/billing') },
],
});
That is the whole setup: ⌘K (or CtrlK) opens it,
? shows every shortcut you registered, and palette.open() opens it from your own button.
Or from the CDN, with no build step:
<link rel="stylesheet" href="https://www.devix.pk/cdn/oss/command-palette@1.0.0/command-palette.min.css">
<script type="module">
import { createCommandPalette } from 'https://www.devix.pk/cdn/oss/command-palette@1.0.0/index.js';
createCommandPalette({ items: [{ id: 'home', title: 'Go home', perform: () => (location.href = '/') }] });
</script>
A command
{
id: 'invoice.new', // stable: recents and updates are keyed on it
title: 'New invoice',
subtitle: 'Start from a blank invoice',
section: 'Create', // the heading it lists under
keywords: ['add', 'bill'], // extra words that should find it
shortcut: 'mod+shift+n', // bound globally, drawn as keys
icon: '<svg …>',
meta: '⏎', // trailing text: a value, a status
perform: () => newInvoice(),
}
Registering from where the command lives
// In a screen, a store, a plugin — anywhere.
const off = palette.register(
{ id: 'row.duplicate', title: 'Duplicate row', perform: () => duplicate(row) },
{ id: 'row.delete', title: 'Delete row', perform: () => remove(row) },
);
// Later, when that screen goes away:
off();
when() keeps a command in the list but hides it while it does not apply:
{ id: 'invoice.send', title: 'Send invoice', when: () => invoice.status === 'draft', perform: send }
Nested pages
{
id: 'theme',
title: 'Change theme',
children: [
{ id: 'theme.light', title: 'Light', perform: () => setTheme('light') },
{ id: 'theme.dark', title: 'Dark', perform: () => setTheme('dark') },
],
}
A breadcrumb appears, Backspace on an empty field steps back, and Esc leaves the
page before it leaves the palette. children can also be a function — including an async one — and a
perform that returns commands opens them as a page.
Searching your server too
createCommandPalette({
items: staticCommands,
minChars: 2,
search: async (query, { signal }) => {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal });
const results = await response.json();
return results.map((row) => ({ id: `doc-${row.id}`, title: row.title, section: 'Documents', perform: () => open(row) }));
},
});
Debounced, aborted when the query moves on, cached per query — and the highlighted row stays where it was when the answer arrives.
What it does without being asked
- One ranking, always the same: match quality, then what you used last, then the order you registered them in.
- A list that stays still: nothing scrolls on open, a new query starts at the top.
- Windowed rows, so a thousand commands cost what twenty do.
- Recents, remembered between visits.
- Shortcuts that never fire while you are typing in a field.
- A cheat sheet on ?.
Updated 15 Sep 2026