mwui
GitHub

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 when g is followed by i within sequenceTimeout. This is the Gmail-style navigation pattern.
  • mod is on Apple platforms and Ctrl everywhere else — the whole reason not to hardcode meta.
  • 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 KeyK resolves to k regardless of layout.
  • Shifted punctuation works either way: "?" and "shift+/" both fire on the same keystroke.

Options

OptionDescription
enabledDetach the listener without unmounting — e.g. only while a panel is focused.
targetElement to listen on. Defaults to document.
enableOnFormElementsFire while an input, textarea, select, or contenteditable has focus. Off by default.
preventDefaultCall preventDefault on a match. On by default, since most shortcuts shadow a browser default.
sequenceTimeoutMilliseconds 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.