@dhruvilshah191999/use-shortcut
Tiny zero-dependency React hook for keyboard shortcuts. Cross-platform, TypeScript-first, ~1KB gzipped.
▶ Live demo — try every feature in your browser (source).
Install
npm install @dhruvilshah191999/use-shortcut
pnpm add @dhruvilshah191999/use-shortcut
yarn add @dhruvilshah191999/use-shortcut
bun add @dhruvilshah191999/use-shortcut
Usage
import { useShortcut } from "@dhruvilshah191999/use-shortcut";
useShortcut("mod+k", () => setOpen(true));
Why this hook?
- Zero dependencies
- Tiny (~1KB gzipped)
- Cross-platform out of the box:
modbecomes Cmd on Mac, Ctrl elsewhere - Smart input field handling: shortcuts don't fire while you're typing (except
Escape) - TypeScript types built in
- Always-fresh callback: no stale closures, no
useCallbackneeded
API
useShortcut(key, callback, options?)
| Parameter | Type | Description |
|---|---|---|
key | string | string[] | A combo like "mod+k", or an array of combos that map to one action. |
callback | (e: KeyboardEvent) => void | Called with the matching event. Always the latest reference. |
options | Options | Optional. See below. |
Combo syntax is case-insensitive and whitespace-tolerant: "Cmd+K", "cmd+k", and " cmd + k " are equivalent. Supported modifier aliases: mod, cmd/command, ctrl/control, alt/option, shift, meta.
Options
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Whether the shortcut is active. |
preventDefault | boolean | false | Call event.preventDefault() on match. |
stopPropagation | boolean | false | Call event.stopPropagation() on match. |
enableInInputs | boolean | false | Fire even while typing in inputs/textarea/select/contenteditable. |
target | RefObject<HTMLElement> | Window | window | Where to attach the listener. Use a ref to scope a shortcut to one element. |
scope | string | string[] | — | Restrict the shortcut to named scope(s); fires only while one is active. See Scopes. |
Examples
Basic
// Cmd+K (Mac) / Ctrl+K (Windows/Linux) to open search
useShortcut("mod+k", () => openSearch());
Cross-platform
// `mod` resolves to Cmd on Mac and Ctrl everywhere else
useShortcut("mod+s", () => save());
Multiple shortcuts for the same action
useShortcut(["mod+k", "/"], () => openSearch());
Modal close on Escape
useShortcut("escape", () => setOpen(false), { enabled: isOpen });
Prevent default browser behavior
// Stop the browser's native "Save page" dialog
useShortcut("mod+s", () => save(), { preventDefault: true });
Scopes (optional)
By default every shortcut is global — it fires whenever its combo matches. If you need
the same key to do different things in different parts of the app (list navigation vs. an
open modal, say), opt into scopes: tag a shortcut with a scope, wrap the relevant
subtree in <ShortcutProvider>, and activate/deactivate scopes with useShortcutScopes().
Shortcuts without a scope stay global and need no provider — scopes are purely additive.
import {
ShortcutProvider,
useShortcut,
useShortcutScopes,
} from "@dhruvilshah191999/use-shortcut";
function App() {
// `list` is active on first render; `modal` is off until we open the modal
return (
<ShortcutProvider initialScopes={["list"]}>
<List />
<Modal />
</ShortcutProvider>
);
}
function List() {
// fires only while the "list" scope is active
useShortcut("j", selectNext, { scope: "list" });
return /* ... */;
}
function Modal() {
const { activate, deactivate } = useShortcutScopes();
function open() {
activate("modal"); // modal shortcuts start firing
deactivate("list"); // list shortcuts pause
}
// fires only while the "modal" scope is active
useShortcut("escape", close, { scope: "modal" });
return /* ... */;
}
useShortcutScopes() returns { activeScopes, activate, deactivate, toggle, set }. A
shortcut can belong to several scopes ({ scope: ["a", "b"] }) and fires if any of
them is active.
If you use a
scopewithout a<ShortcutProvider>, the shortcut degrades to global behavior and warns once in development.
Caveats
- No sequence support (e.g.,
g g). - Scopes are opt-in via
<ShortcutProvider>; without one, every shortcut is global (you can also narrow a single shortcut to an element with thetargetoption). - No priority/conflict resolution — multiple handlers for the same combo all fire. Use scopes to gate which handlers are live.
When NOT to use this
If you need key sequences (e.g., g g), a help-modal generator, or a key-recorder UI, use react-hotkeys-hook or TanStack Hotkeys.
License
MIT