useHotkeys
A hook that binds keyboard shortcuts with modifier and sequence support.
Try ⌘/CtrlK, then G I, then ?
No shortcut fired yet.
Installation
Usage
Declare a shortcut where the thing it does lives, instead of routing every key through one global
keydown switch.
import { useHotkeys } from "@/hooks/use-hotkeys";
function CommandMenu() {
const [open, setOpen] = React.useState(false);
useHotkeys("mod+k", () => setOpen((value) => !value));
return <CommandDialog open={open} onOpenChange={setOpen} />;
}Syntax
- Combinations join with
+:"mod+k","ctrl+shift+p","alt+arrowleft". - Sequences separate with a space:
"g i"fires whengis followed byiwithinsequenceTimeout. This is the Gmail-style navigation pattern. modis⌘on Apple platforms andCtrleverywhere else — the whole reason not to hardcodemeta.- Aliases are accepted for the names people actually type:
esc,del,up,down,left,right,return,cmd,option,space.
Pass an array to bind several shortcuts to the same handler:
useHotkeys(["mod+s", "ctrl+s"], save);Matching
Modifiers must match exactly, so "k" does not fire on ⌘K. Keys are matched against both
event.key and a normalized event.code, which means two things worth knowing:
- Letter shortcuts work on non-QWERTY layouts, because
KeyKresolves tokregardless of layout. - Shifted punctuation works either way:
"?"and"shift+/"both fire on the same keystroke.
Options
| Option | Description |
|---|---|
enabled | Detach the listener without unmounting — e.g. only while a panel is focused. |
target | Element to listen on. Defaults to document. |
enableOnFormElements | Fire while an input, textarea, select, or contenteditable has focus. Off by default. |
preventDefault | Call preventDefault on a match. On by default, since most shortcuts shadow a browser default. |
sequenceTimeout | Milliseconds allowed between sequence steps. Defaults to 1000. |
Behavior
- The handler is read from a ref, so it always sees current state and never re-binds the listener.
- Auto-repeat is ignored, so holding a key fires once.
- Pressing a modifier on its own never starts or breaks a sequence.
- Sequence progress resets after a match, a timeout, or a non-matching key.