Keyboard shortcuts make power users more productive. Here is how to add them properly.
Step 1: Custom Hook for Key Bindings
// hooks/useKeyboardShortcut.ts
"use client";
import { useEffect, useCallback } from "react";
interface ShortcutOptions {
ctrl?: boolean;
meta?: boolean;
shift?: boolean;
alt?: boolean;
}
export function useKeyboardShortcut(
key: string,
callback: () => void,
options: ShortcutOptions = {}
) {
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
// Don't trigger in input fields
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
(e.target instanceof HTMLElement && e.target.isContentEditable)
) {
return;
}
const modifierMatch =
(options.ctrl ? e.ctrlKey : !e.ctrlKey || options.meta) &&
(options.meta ? e.metaKey : !e.metaKey || options.ctrl) &&
(options.shift ? e.shiftKey : !e.shiftKey) &&
(options.alt ? e.altKey : !e.altKey);
// Support both Ctrl and Cmd
const ctrlOrMeta = options.ctrl || options.meta;
const ctrlMetaMatch = ctrlOrMeta
? e.ctrlKey || e.metaKey
: !e.ctrlKey && !e.metaKey;
if (e.key.toLowerCase() === key.toLowerCase() && ctrlMetaMatch) {
e.preventDefault();
callback();
}
},
[key, callback, options]
);
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]);
}
Step 2: Basic Usage
"use client";
import { useKeyboardShortcut } from "@/hooks/useKeyboardShortcut";
import { useRouter } from "next/navigation";
export function AppShortcuts() {
const router = useRouter();
// Cmd+K: Open command palette
useKeyboardShortcut("k", () => openCommandPalette(), { meta: true });
// Cmd+/: Toggle shortcuts help
useKeyboardShortcut("/", () => openShortcutsDialog(), { meta: true });
// G then H: Go home (vim-style)
useKeyboardShortcut("h", () => router.push("/"));
return null; // Invisible component that adds shortcuts
}
Step 3: Shortcut Registry
// lib/shortcuts.ts
export interface Shortcut {
key: string;
label: string;
description: string;
category: string;
modifiers?: ("Ctrl" | "Cmd" | "Shift" | "Alt")[];
}
export const shortcuts: Shortcut[] = [
{
key: "k",
label: "K",
description: "Open command palette",
category: "General",
modifiers: ["Cmd"],
},
{
key: "/",
label: "/",
description: "Show keyboard shortcuts",
category: "General",
modifiers: ["Cmd"],
},
{
key: "b",
label: "B",
description: "Toggle sidebar",
category: "Navigation",
modifiers: ["Cmd"],
},
{
key: "n",
label: "N",
description: "New item",
category: "Actions",
modifiers: ["Cmd"],
},
{
key: "s",
label: "S",
description: "Save changes",
category: "Actions",
modifiers: ["Cmd"],
},
{
key: "Escape",
label: "Esc",
description: "Close dialog / Cancel",
category: "General",
},
];
Step 4: Shortcuts Help Dialog
"use client";
import { useState } from "react";
import { useKeyboardShortcut } from "@/hooks/useKeyboardShortcut";
import { shortcuts } from "@/lib/shortcuts";
export function ShortcutsDialog() {
const [open, setOpen] = useState(false);
useKeyboardShortcut("/", () => setOpen(true), { meta: true });
useKeyboardShortcut("Escape", () => setOpen(false));
if (!open) return null;
const categories = [...new Set(shortcuts.map((s) => s.category))];
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div
className="absolute inset-0 bg-black/50"
onClick={() => setOpen(false)}
/>
<div className="relative w-full max-w-lg rounded-xl border bg-white p-6 shadow-2xl dark:border-gray-700 dark:bg-gray-900">
<h2 className="text-lg font-bold">Keyboard Shortcuts</h2>
<div className="mt-4 space-y-6">
{categories.map((category) => (
<div key={category}>
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-gray-500">
{category}
</h3>
<div className="space-y-2">
{shortcuts
.filter((s) => s.category === category)
.map((shortcut) => (
<div
key={shortcut.key}
className="flex items-center justify-between"
>
<span className="text-sm text-gray-600 dark:text-gray-400">
{shortcut.description}
</span>
<div className="flex gap-1">
{shortcut.modifiers?.map((mod) => (
<kbd
key={mod}
className="rounded border bg-gray-100 px-1.5 py-0.5 text-xs dark:border-gray-600 dark:bg-gray-800"
>
{mod}
</kbd>
))}
<kbd className="rounded border bg-gray-100 px-1.5 py-0.5 text-xs dark:border-gray-600 dark:bg-gray-800">
{shortcut.label}
</kbd>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
);
}
Step 5: Sequence Shortcuts (Vim-Style)
// hooks/useSequenceShortcut.ts
"use client";
import { useEffect, useRef, useCallback } from "react";
export function useSequenceShortcut(
sequence: string[],
callback: () => void,
timeout = 1000
) {
const buffer = useRef<string[]>([]);
const timer = useRef<NodeJS.Timeout>();
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement
) {
return;
}
buffer.current.push(e.key.toLowerCase());
// Reset timer
if (timer.current) clearTimeout(timer.current);
timer.current = setTimeout(() => {
buffer.current = [];
}, timeout);
// Check if sequence matches
const lastN = buffer.current.slice(-sequence.length);
if (
lastN.length === sequence.length &&
lastN.every((k, i) => k === sequence[i].toLowerCase())
) {
buffer.current = [];
callback();
}
},
[sequence, callback, timeout]
);
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]);
}
// Usage: Press "g" then "h" to go home
useSequenceShortcut(["g", "h"], () => router.push("/"));
useSequenceShortcut(["g", "s"], () => router.push("/settings"));
useSequenceShortcut(["g", "p"], () => router.push("/projects"));
Step 6: Keyboard Shortcut Tooltip
export function ShortcutHint({ keys }: { keys: string[] }) {
return (
<div className="hidden items-center gap-0.5 sm:flex">
{keys.map((key, i) => (
<kbd
key={i}
className="rounded border bg-gray-50 px-1 py-0.5 text-[10px] text-gray-400 dark:border-gray-700 dark:bg-gray-800"
>
{key}
</kbd>
))}
</div>
);
}
// Usage in a button
<button className="flex items-center gap-2">
<span>Search</span>
<ShortcutHint keys={["⌘", "K"]} />
</button>
Need Power-User Features?
We build web applications with keyboard shortcuts, command palettes, and productivity-focused UX. Contact us to discuss your project.