Skip to content

useKey

provider-dependenthotkeykeyboard

Bind keyboard shortcuts to handlers with scoped, document-level, or container-scoped listening. Supports single key, array of keys, or key map overloads.

useKey is the core binding primitive of @diffgazer/keys. It registers one or more hotkeys with the nearest KeyboardProvider and runs your handler when a matching combo fires. Bindings are scope-aware: the provider resolves which handler wins when several match, and a binding is automatically removed when its component unmounts.

tsx
useKey("Escape", () => setOpen(false))

When to use it

  • Application shortcuts — Cmd+K to open a palette, Escape to close an overlay, ? to show help.
  • Component shortcuts that should only fire while a region has focus (pass containerRef + focusWithinOnly).
  • Layered UI where a deeper surface (dialog, drawer) should shadow a shortcut owned by the page underneath — combine with useScope.

Higher-level navigation hooks (useScopedNavigation, useFocusZone, useActionRowNavigation) are built on useKey; reach for it directly when you need an individual shortcut rather than list or zone movement.

Installation

Requires KeyboardProvider and the @diffgazer/keys package, which is not public on npm yet.

Diffgazer packages are not yet published to npm. Until the first release, pack @diffgazer/ui and @diffgazer/keys from the repository and install those tarballs.

@diffgazer/keys requires no CSS or Tailwind setup.

Parameters

NameTypeDefaultDescription
hotkeyrequiredstring | readonly string[] | Record<string, KeyHandler>A single key combo, an array of key combos, or a map of key combos to handlers.
handlerKeyHandlerCallback invoked when the hotkey fires. Not used with the key-map overload.
options.enabledbooleantrueWhether the binding is active.
options.scopestring | nullExplicit keyboard scope to register the binding under. Pass null to skip registration. Defaults to the nearest active KeyboardProvider scope for this hook's declaration order.
options.allowInInputbooleanfalseAllow the hotkey to fire when an input, textarea, or contenteditable element is focused.
options.containerRefRefObject<HTMLElement | null>DOM boundary used by focusWithinOnly to restrict when the handler fires.
options.focusWithinOnlybooleanfalseOnly fire the handler when focus is within the container element.
options.preventDefaultbooleanfalseCall preventDefault() on the keyboard event when the hotkey matches.

The hotkey string uses the hotkey format: a key name with optional ctrl+, meta+, alt+, and shift+ modifiers (for example "meta+k", "shift+?", "Escape").

Returns

voidThis hook does not return a value.

Behavior

ConcernBehavior
Editable targetsIgnored by default. A shortcut does not fire while a text-like input, a textarea, or an editable content element is focused unless allowInInput is set. Non-text controls such as select, checkbox, radio, range, and button inputs are allowed by default.
Focus scopingWith containerRef + focusWithinOnly, the handler only runs while focus is inside the container.
Scope resolutionWhen several bindings match the same combo, the handler in the deepest active scope runs first.
DecliningReturn exactly false from a handler to fall through to the next lower-priority handler for that combo. Any other return value consumes the event.
preventDefaultWhen enabled, the browser default is prevented after a handler accepts. A handler that returns false leaves the default available unless a later matching handler accepts and prevents it.
CleanupBindings are removed on unmount, when enabled is false, or when scope is null.

Examples

Basic hotkey binding

Preview

Key map with multiple bindings

Preview

Container-scoped shortcut

Preview

Notes

Three overloads

useKey supports three call signatures: single key + handler, array of keys + handler, and a Record<string, KeyHandler> map (no separate handler argument).

Provider-dependent

useKey registers handlers when a KeyboardProvider is present. Without a provider it is a no-op.

Scope-aware

Handlers respect the scope stack managed by useScope. Deeper scopes take priority over shallower ones.

Declining a match

Return exactly false from a handler to let the next lower-priority matching handler in the active scope run. Other return values are ignored. When a handler with preventDefault enabled accepts the match (returns anything other than false), event.preventDefault() runs after that handler returns. A declining handler never prevents the default.

Edge cases

  • No provider. Without a KeyboardProvider ancestor, useKey is a no-op — it never throws, so a copied component degrades to "no shortcut" rather than crashing.
  • scope: null. Passing null skips registration entirely while keeping the Hook call in place, which is the supported way to disable a binding conditionally without violating the Rules of Hooks.
  • Key map overload. useKey({ "ctrl+b": ..., Escape: ... }) registers several combos at once; do not pass a separate handler argument with this form.
  • useScope — push a named scope so deeper surfaces shadow page-level shortcuts.
  • KeyboardProvider — the context that dispatches matched keys.
  • Hotkey format — modifier and key-name reference.

Source

Highlighted source loads after this disclosure opens. Browse the source repository.