Every polished app eventually grows a set of keyboard shortcuts. Press command+s to save, / to focus search, n to create something new, esc to dismiss a modal. The behavior sounds trivial until you actually build it: now you are juggling addEventListener, normalizing modifier keys across operating systems, de-duplicating auto-repeat events, and remembering to tear everything down on unmount so you do not leak listeners. That boilerplate multiplies with every shortcut you add.
React Hot Keys collapses all of that into a single declarative component. You wrap a region of your UI in <Hotkeys>, hand it a string describing the combos you care about, and provide onKeyDown / onKeyUp callbacks. Binding happens on mount, cleanup happens on unmount, and the cross-browser key detection is handled for you. It is published on npm as react-hot-keys, and it shines anywhere you want shortcuts expressed as JSX rather than imperative effect code.
A Quick Word on the Name
Before anything else, a genuine gotcha worth flagging. The package you want is react-hot-keys — with the extra dash. There is a completely separate, unrelated, and effectively unmaintained package literally named react-hotkeys (no dash) by a different author whose last release landed back in 2019. They are not the same project. If you install react-hotkeys expecting the <Hotkeys> component described here, you will end up with the wrong library.
So the rule is simple: install react-hot-keys, import from react-hot-keys, and ignore the near-identical impostor. The repository's display name is "React Hotkeys," which is where the confusion originates, but the npm identity is always the dashed one.
What Makes It Handy
- Declarative binding — shortcuts live in your JSX as a
keyNamestring, not buried inuseEffectplumbing. - Keydown and keyup — explicit
onKeyUpsupport, which is great for movement-style interactions where pressing starts an action and releasing ends it. - Automatic lifecycle management — listeners are registered on mount and cleaned up on unmount, with no manual
unbind. - Modifier normalization — combos like
command+s,ctrl+shift+k, andalt+sare parsed and matched consistently, courtesy of the underlying hotkeys-js engine. - Form-field safety by default — shortcuts are ignored while the user types in an input, textarea, or contentEditable region, so typing an
nin a search box does not trigger your "new item" shortcut. - A tiny footprint — exactly one runtime dependency (hotkeys-js), shipped as both CommonJS and ESM with TypeScript types included.
Under the hood, React Hot Keys is a thin React wrapper around hotkeys-js, the dependency-free keyboard micro-library by the same author. hotkeys-js does the low-level key detection; React Hot Keys adapts it to React's component lifecycle so you never call the imperative API yourself.
Installation
Add it with your package manager of choice:
npm i react-hot-keys
yarn add react-hot-keys
pnpm add react-hot-keys
It expects React and React DOM >=16.9.0 as peer dependencies, so it works comfortably in any reasonably modern React app.
Getting Off the Ground
Your First Shortcut
The whole API surface is one component. Give it a keyName and a callback, wrap some children, and you are listening:
import { useState } from 'react';
import Hotkeys from 'react-hot-keys';
export default function HotkeysDemo() {
const [output, setOutput] = useState('Press shift+a or alt+s');
const onKeyDown = (shortcut: string) => {
setOutput(`onKeyDown ${shortcut}`);
};
const onKeyUp = (shortcut: string) => {
setOutput(`onKeyUp ${shortcut}`);
};
return (
<Hotkeys keyName="shift+a,alt+s" onKeyDown={onKeyDown} onKeyUp={onKeyUp}>
<div style={{ padding: 50 }}>{output}</div>
</Hotkeys>
);
}
The keyName prop is a comma-separated list of combos. Here "shift+a,alt+s" registers two distinct shortcuts at once. When either fires, the matched combo string is handed to your callback so you can branch on which one was pressed. The + joins a modifier to a key, and the supported aliases come straight from hotkeys-js: shift, alt (also option), ctrl (also control), and command, plus named keys like space, enter, and the arrow keys.
Reading the Full Callback
Each handler actually receives three arguments, and the extra two are worth knowing about:
import Hotkeys from 'react-hot-keys';
import type { HotkeysEvent } from 'hotkeys-js';
function SaveRegion() {
const onKeyDown = (
shortcut: string,
event: KeyboardEvent,
handle: HotkeysEvent,
) => {
event.preventDefault();
console.log('combo:', shortcut);
console.log('native key:', handle.key);
console.log('scope:', handle.scope);
};
return (
<Hotkeys keyName="command+s,ctrl+s" onKeyDown={onKeyDown}>
<p>Press Cmd+S (or Ctrl+S) to save.</p>
</Hotkeys>
);
}
The first argument is the matched combo string. The second is the native KeyboardEvent, which you will reach for whenever you need to call preventDefault() — essential for overriding the browser's own command+s save dialog. The third is the HotkeysEvent object from hotkeys-js, carrying useful metadata such as the resolved key, the full shortcut, and the active scope.
Letting Held Keys Repeat
By default React Hot Keys de-duplicates auto-repeat events, so holding a key down fires your onKeyDown exactly once. That is usually what you want for discrete commands. But for continuous interactions — think nudging a selection or panning a canvas while a key is held — you want the repeat. Flip the allowRepeat prop on:
import { useState } from 'react';
import Hotkeys from 'react-hot-keys';
export default function NudgeBox() {
const [count, setCount] = useState(0);
return (
<Hotkeys
keyName="right"
allowRepeat
onKeyDown={() => setCount((c) => c + 1)}
>
<div style={{ padding: 50 }}>Held right-arrow steps: {count}</div>
</Hotkeys>
);
}
With allowRepeat enabled, holding the right-arrow key keeps incrementing the counter as the OS fires repeat events, instead of registering a single press.
Going Further
Capturing Shortcuts Inside Form Fields
This is the behavior that surprises people most often, so it is worth understanding precisely. By default, React Hot Keys refuses to fire when the keystroke originates inside an INPUT, SELECT, TEXTAREA, or contentEditable element. That default is a feature: it stops a global s shortcut from hijacking the letter "s" while someone fills out a form.
When you genuinely do want a shortcut to fire even inside inputs — for example, a command+enter submit binding within a rich text composer — override the default by supplying your own filter:
import Hotkeys from 'react-hot-keys';
function ComposerSubmit({ onSubmit }: { onSubmit: () => void }) {
return (
<Hotkeys
keyName="command+enter,ctrl+enter"
filter={() => true}
onKeyDown={(_shortcut, event) => {
event.preventDefault();
onSubmit();
}}
>
<textarea placeholder="Write something, then Cmd+Enter to send" />
</Hotkeys>
);
}
The filter function receives the native event and returns a boolean: true lets the shortcut proceed, false suppresses it. Returning true unconditionally opts every keystroke in, including those from form fields. You can also be selective — inspect event.target and allow the shortcut only for specific elements.
Reaching for the Imperative Handle
Version 3 rewrote the component to a function component built on forwardRef, which means you can grab an imperative ref and ask it about the current key state. The exposed handle includes isKeyDown, the active handle, and the component's props:
import { useRef, useState } from 'react';
import Hotkeys from 'react-hot-keys';
export default function RefAwareDemo() {
const [output, setOutput] = useState('Hold shift+a');
const hotkeysRef = useRef<any>(null);
const onKeyDown = (shortcut: string) => {
setOutput(`pressed ${shortcut} — isKeyDown=${hotkeysRef.current?.isKeyDown}`);
};
return (
<Hotkeys
ref={hotkeysRef}
keyName="shift+a,alt+s"
onKeyDown={onKeyDown}
onKeyUp={() => setOutput('released')}
>
<div style={{ padding: 50, border: '1px solid #ccc' }}>{output}</div>
</Hotkeys>
);
}
This is handy when an external piece of logic — say a render loop or an event somewhere else in your tree — needs to know whether a hotkey is currently held without waiting for a callback to fire.
Custom Separators and Conditional Activation
Two more tools round out the practical toolkit. Version 3 added a splitKey prop that lets you change the delimiter used to parse keyName, which matters when a literal comma is itself part of a binding and the default comma separator would misinterpret it. And because there is no first-class scope API, the idiomatic way to "scope" shortcuts to a particular context is conditional mounting — only render the <Hotkeys> component when the shortcut should be live:
import Hotkeys from 'react-hot-keys';
function Modal({ open, onClose }: { open: boolean; onClose: () => void }) {
if (!open) return null;
return (
<div className="modal">
<Hotkeys keyName="esc" onKeyDown={onClose}>
<p>Press Esc to close.</p>
</Hotkeys>
</div>
);
}
Here the esc binding only exists while the modal is open. When open flips to false, the component unmounts and the listener is automatically removed. Combined with the disabled prop, which silences both callbacks without unmounting, you have enough control to manage shortcut contexts without any explicit scope machinery.
Where It Fits
React Hot Keys sits in a small, opinionated niche. It is the component-shaped sibling to the more popular hook-based alternatives that also build on hotkeys-js. If your codebase leans declarative — especially if you appreciate seeing shortcuts expressed as JSX next to the UI they affect — or if you have older class components still in the mix, the <Hotkeys> wrapper fits naturally. Its explicit onKeyUp support is a real differentiator for interactions where releasing a key matters as much as pressing it.
It is honest about its boundaries, too. There is no first-class scope manager, so heavy scope juggling is better served by dropping down to hotkeys-js directly. The fn key cannot be intercepted, a limitation inherited from the engine underneath. And the component-per-shortcut model can grow verbose if you have dozens of global bindings, where a single hook registering many combos might read more compactly.
But for the common case — a handful of declarative, lifecycle-safe shortcuts wired to clear callbacks, with form fields respectfully left alone by default — React Hot Keys does exactly what you want with almost no ceremony. You wrap, you name your keys, you handle the press, and the binding and cleanup simply take care of themselves.