Command palette

View on GitHub

Add a global command

Worked example: a "Go to my Issues" command (a page used purely to demonstrate the shape) that is reachable from the palette and from the keyboard as g i, on every page.

When this is the right shape

Add it to useGlobalCommands only for something genuinely global — navigation between top-level areas, preferences, account, help. A command that needs data only one page has (a project's views, an issue's fields) belongs to that page: see the page-level variant below and guide 2Command paletteAdd a contextual command with nested stepsWorked example: the issue page's Change status…. It depends on the current issue, opens a second list ("a step") of the statuses the workflow allows, hides….

Step — Add the command

File: resources/js/hooks/useGlobalCommands.ts

{
    id: 'nav.issues',
    title: 'Go to my Issues',
    group: 'navigation',
    icon: 'ListChecks',
    shortcut: 'g i',
    category: 'Navigation',
    keywords: ['assigned', 'mine'],
    perform: () => router.visit('/issues/mine'),
},
FieldMeaning
idStable and unique. This is what runCommand('nav.issues') takes, so a button can run a command without knowing its keybinding.
titleThe row text; the fuzzy search highlights it.
subtitleOptional muted meta text after the title.
groupnavigation, creation, preferences, account or help (also suggested, recent, issues, projects, people, which the palette fills itself). The on-screen order is COMMAND_GROUP_ORDER in types/Commands.ts.
iconAny key of lucide's icons (the type enforces it).
keywordsExtra words the search matches but never highlights.
shortcutOptional. A string ('alt+i', 'mod+k', a sequence like 'g i') or an array for several bindings. It is registered with the shortcut engine while the command is registered, and shown as a chip.
categoryWhich section the shortcut gets in the help view; defaults from group.
scope'project' or 'issue' makes the shortcut win over a global one with the same key.
whenReturn false to hide the command and disable its shortcut.
performWhat happens. The palette closes first unless keepOpen is set.

Use router.visit(...) (Inertia) for navigation, never window.location.href: it keeps the navigation inside Inertia's SPA-style routing.

Sequences (g i) need no extra setup. The engine matches the longest run of recent keys, so g p ("Go to Projects") wins over the single-key p ("Create project"), and it is ignored while an input has focus.

A command owned by one component

When the command needs the component's own state (the sidebar opens its "new project" modal), register it from that component with useCommands, and memoize the array:

File: resources/js/Components/Organisms/Sidebar/Sidebar.tsx

const commands = useMemo(
    (): Command[] => [
        {
            id: 'project.create',
            title: 'Create project',
            group: 'creation',
            icon: 'FolderPlus',
            shortcut: 'p',
            keywords: ['new project'],
            perform: () => setIsNewProjectModalOpen(true),
        },
    ],
    [],
);

useCommands(commands);

It exists exactly while the component is mounted. A button elsewhere can run it by id with useCommandPalette().runCommand('project.create') — prefer that over the deprecated triggerShortcut('p'), which couples a button to a keybinding.

Tests

File: resources/js/hooks/useGlobalCommands.test.ts

The file mocks useCommands and reads back what the hook registered:

test('go to my issues has its own shortcut and visits the page', () => {
    render();

    expect(byId('nav.issues').shortcut).toBe('g i');

    byId('nav.issues').perform?.();
    expect(mocks.router.visit).toHaveBeenCalledWith('/issues/mine');
});

The keydown → action path, sequences and mod are covered once in CommandContext.test.tsx and ShortcutContext.test.tsx; a new command does not repeat them.