Most apps eventually need keyboard shortcuts - a command palette on Cmd/Ctrl+K, Esc to close a modal, mod+S to save. I kept rewriting the same keydown boilerplate, so I packaged it into a tiny hook.
@dhruvilshah191999/use-shortcut - zero dependencies, ~1KB gzipped, TypeScript-first.
🔗 Live demo: https://use-shortcut-playground.vercel.app/
📦 npm: https://www.npmjs.com/package/@dhruvilshah191999/use-shortcut
💻 Source: https://github.com/dhruvilshah191999/use-shortcut
Install
npm install @dhruvilshah191999/use-shortcut
The basics
import { useShortcut } from "@dhruvilshah191999/use-shortcut";
function SearchButton() {
const [open, setOpen] = useState(false);
useShortcut("mod+k", () => setOpen(true));
// ...
}
That's the whole API for simple cases: a combo string, a callback, done.
What makes it nice to use
mod is cross-platform out of the box
useShortcut("mod+s", save, { preventDefault: true }); // ⌘S on Mac, Ctrl+S elsewhere
Combos are case-insensitive and whitespace-tolerant ("Cmd+K", "cmd+k", " cmd + k " all work), with aliases like cmd↔command, ctrl↔control, alt↔option.
Smart input handling
By default, shortcuts don't fire while you're typing in an <input>, <textarea>, <select>, or contenteditable - except Escape, which always fires (closing a modal should work even with focus in a field). You can opt back in with enableInInputs: true.
Always-fresh callback (no useCallback needed)
The hook always calls the latest callback via a ref, so you never get a stale closure:
function Counter() {
const [count, setCount] = useState(0);
// logs the CURRENT count every time, no useCallback required
useShortcut("c", () => console.log(count));
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}
Optional scopes (when you outgrow "global")
Every shortcut is global by default. When you need the same key to mean different things in different parts of the UI, wrap a subtree in a provider and gate shortcuts by scope:
import {
ShortcutProvider,
useShortcut,
useShortcutScopes,
} from "@dhruvilshah191999/use-shortcut";
function App() {
return (
<ShortcutProvider initialScopes={["list"]}>
<List />
<Modal />
</ShortcutProvider>
);
}
function Modal() {
const { activate, deactivate } = useShortcutScopes();
useShortcut("escape", close, { scope: "modal" }); // fires only when "modal" is active
// open(): activate("modal"); deactivate("list");
}
Scopes are fully opt-in - if you never use a scope, nothing changes and you don't need the provider.
The whole API
useShortcut(
key: string | string[],
callback: (e: KeyboardEvent) => void,
options?: {
enabled?: boolean; // default: true
preventDefault?: boolean; // default: false
stopPropagation?: boolean; // default: false
enableInInputs?: boolean; // default: false
target?: RefObject<HTMLElement> | Window; // default: window
scope?: string | string[]; // optional, opt-in
}
): void
Try it
The live demo exercises every feature with an on-screen event log — open it and start pressing keys.
If you find it useful, a ⭐ on the repo is appreciated. Feedback and issues welcome!
Top comments (0)