Keyboard shortcuts
View on GitHubRegister a global shortcut
Worked example: adding alt+d as a global "Go to Dashboard" shortcut
alternative — wait, that one already exists as alt+b; the real
worked example here is adding alt+i for "Go to my Issues" (a page
that doesn't have a route yet, used purely to demonstrate the
registration shape) — a shortcut that should work from anywhere in the
app, not just one page, following the exact pattern ctrl+k/alt+p/
alt+b/ctrl+f already use.
When this is the right shape, vs. guide 1
Register directly inside ShortcutProvider (this guide) only for a
shortcut that's genuinely global — navigation between top-level areas,
or an action meaningful regardless of what page is open (the help
modal itself, ctrl+k). Everything else — an action specific to one
page's content, like Sidebar.tsx's p or a hypothetical per-issue
shortcut — belongs in that component via useShortcuts() instead (see
01-register-a-component-scoped-shortcut.md).
Registering something page-specific here would make it fire even on
pages where the action doesn't make sense.
Step — Add it to the existing global registerBatch() call
File: resources/js/context/ShortcutContext.tsx
useEffect(() => {
return registerBatch([
{
key: 'alt+p',
description: 'Go to Projects',
category: 'Navigation',
action: () => router.visit('/projects'),
},
{
key: 'alt+b',
description: 'Go to Dashboard',
category: 'Navigation',
action: () => router.visit('/'),
},
{
key: 'alt+i',
description: 'Go to my Issues',
category: 'Navigation',
action: () => router.visit('/issues/mine'),
},
{
key: 'ctrl+f',
description: 'Focus Search',
category: 'Search',
action: () => {
const searchInput = document.querySelector(
'input[type="text"]',
) as HTMLInputElement;
if (searchInput) searchInput.focus();
},
},
]);
}, [registerBatch]);
This is the only useEffect in ShortcutProvider that batches
multiple navigation shortcuts together — the help-modal (?//) and
command-palette (ctrl+k) shortcuts each get their own dedicated
useEffect/register() call instead, purely because they were added
at different times, not because of any rule about which shape to use.
Either is fine for a new global shortcut; adding to the existing
registerBatch() array (shown above) is slightly less code for one
more navigation-style entry.
router.visit(...) (the Inertia client-side navigation call) is the
right primitive here, not window.location.href = ... — it keeps the
navigation inside Inertia's SPA-style routing instead of forcing a
full page reload.
Tests
File: resources/js/context/ShortcutContext.test.tsx
Add a case right next to 'alt+p navigates to /projects via the router'/'alt+b navigates to / via the router', following their
exact shape — dispatch a real keydown (through whatever
dispatchKeydown() helper the file already defines) rather than
calling .action() directly, since the point of this file is to
cover the actual key-matching logic too, not just that the definition
exists:
test('alt+i navigates to /issues/mine via the router', () => {
renderHook(() => useShortcuts(), { wrapper });
act(() => {
dispatchKeydown({ key: 'i', altKey: true });
});
expect(mockRouter.visit).toHaveBeenCalledWith('/issues/mine');
});
mockRouter here is the file's own hoisted @inertiajs/react mock
(vi.mock('@inertiajs/react', () => ({ router: mockRouter }))) —
reuse it as-is, don't add a second mock for the same module.
