react-hotkeys-hook and react-shortcuts are both libraries designed to manage keyboard events in React applications, but they serve different architectural needs. react-hotkeys-hook is a modern, hook-based solution that allows developers to define hotkeys directly within functional components, handling scope, prevention of default browser actions, and cleanup automatically. react-shortcuts is an older, higher-order component (HOC) based library that focuses on defining a global map of shortcuts, often used in conjunction with a specific key mapping strategy, and relies on class components or wrapping functional components, which can feel less native in modern React codebases.
Handling keyboard interactions is a critical part of building accessible and efficient web applications. Whether you are building a complex dashboard, a text editor, or a data grid, users expect reliable keyboard shortcuts. In the React ecosystem, react-hotkeys-hook and react-shortcuts represent two different eras of development patterns. Let's break down how they differ in architecture, usage, and suitability for modern projects.
The most significant difference lies in how these libraries integrate with your components. This dictates not just how you write code, but how you think about component lifecycle and state.
react-hotkeys-hook is built from the ground up for React Hooks. It provides a simple useHotkeys function that you call inside your functional component. This aligns perfectly with modern React practices, keeping logic co-located with the UI it affects.
// react-hotkeys-hook: Direct hook usage
import { useHotkeys } from 'react-hotkeys-hook';
function Editor() {
const save = () => console.log('Saving...');
// Define hotkey directly in the component body
useHotkeys('ctrl+s', save, { preventDefault: true });
return <div>Press Ctrl+S to save</div>;
}
react-shortcuts, by contrast, relies on the Higher-Order Component (HOC) pattern, which was common before hooks existed. It wraps your component and injects shortcut handling props. This often forces you to structure your code differently, potentially leading to "wrapper hell" or requiring class components for easier state access in older patterns.
// react-shortcuts: HOC pattern
import { shortcuts } from 'react-shortcuts';
class Editor extends React.Component {
handleSave = () => console.log('Saving...');
render() {
return <div>Press Ctrl+S to save</div>;
}
}
// Wrap the component with the HOC
export default shortcuts(Editor, {
'editor.save': ['ctrl+s']
});
Managing when a shortcut is active is crucial. You don't want a "delete" shortcut firing while a user is typing a comment in a text box.
react-hotkeys-hook excels here with built-in options. It automatically detects if an input field is focused and can ignore shortcuts in that context. It also handles scoping naturally because the hook is tied to the component's mount lifecycle. When the component unmounts, the listener is removed.
// react-hotkeys-hook: Automatic input filtering and scoping
import { useHotkeys } from 'react-hotkeys-hook';
function DataTable() {
const deleteRow = () => console.log('Row deleted');
// 'enableOnFormTags' prevents firing when inputs are focused
useHotkeys('delete', deleteRow, {
enableOnFormTags: false,
scopes: ['table-view']
});
return <input placeholder="Type here (delete won't work)" />;
}
react-shortcuts requires more manual setup for scoping. Since it often relies on a global map or a top-level provider, you must explicitly manage which "scope" is active via context or props. Filtering out form tags usually requires custom logic or configuration at the provider level, which can be less intuitive for localized features.
// react-shortcuts: Manual scope management concept
// Typically requires a top-level ShortcutsProvider and explicit scope switching
<ShortcutsProvider scopes={['table-view']}>
<DataTable />
</ShortcutsProvider>
// Inside the config map, you might need custom guards
// to check event targets manually if not globally configured
Both libraries allow you to define complex key combinations, but the developer experience differs.
react-hotkeys-hook uses a straightforward string syntax that supports modifiers like ctrl, shift, alt, and meta. It feels very natural to read and write.
// react-hotkeys-hook: Readable string syntax
useHotkeys('cmd+shift+k', openCommandPalette);
useHotkeys('up, down', navigateList); // Supports sequences
react-shortcuts also supports combinations but often ties them to named actions in a configuration object. This indirection can be useful for centralizing shortcut definitions but adds verbosity for simple, component-local actions.
// react-shortcuts: Named action mapping
const shortcutConfig = {
'app.openPalette': ['meta+shift+k'],
'list.navigate': ['up', 'down']
};
// The component then maps these names to handlers
Memory leaks from forgotten event listeners are a common pitfall in React apps.
react-hotkeys-hook handles this automatically. Because it uses the useEffect hook internally, it registers the listener on mount and cleans it up on unmount. You don't need to write any teardown code.
// react-hotkeys-hook: Automatic cleanup
function Modal() {
// Listener is removed automatically when Modal unmounts
useHotkeys('esc', closeModal);
return <div>Modal Content</div>;
}
react-shortcuts manages listeners through its HOC or provider. While it generally handles cleanup when the wrapped component unmounts, the indirect nature of HOCs can sometimes make debugging lifecycle issues harder. If you are manually attaching listeners alongside this library, you bear the burden of cleanup yourself.
You need shortcuts that work anywhere in the app, like ? for help or g then h for home.
react-hotkeys-hook Approach:
You can place a hook in your root layout or a dedicated global component. The scopes feature helps organize these.
// GlobalShortcuts.js
import { useHotkeys } from 'react-hotkeys-hook';
export function GlobalShortcuts() {
useHotkeys('?', () => openHelpModal(), { scopes: ['global'] });
return null; // Renders nothing, just listens
}
react-shortcuts Approach:
This is where react-shortcuts was originally designed to shine, using a central map.
// App.js with react-shortcuts
const globalMap = {
'global.help': ['?'],
'global.home': ['g', 'h']
};
// Wrapped App component handles the dispatch
You are building a text editor where bold (Ctrl+B) only works when the editor is focused.
react-hotkeys-hook Approach:
Simply drop the hook inside the editor component. It's self-contained.
function TextEditor() {
const makeBold = () => document.execCommand('bold');
// Only active when this component is mounted
useHotkeys('ctrl+b', makeBold, { enableOnFormTags: true });
return <textarea />;
}
react-shortcuts Approach:
You must ensure the HOC-wrapped editor is the one receiving focus events and that the global map respects the current active scope, which often requires passing props down to indicate the active context.
// Requires careful prop drilling or context to ensure
// the 'editor' scope is the active one in the provider
<EditorComponent isActive={true} />
| Feature | react-hotkeys-hook | react-shortcuts |
|---|---|---|
| Pattern | React Hooks (useHotkeys) | Higher-Order Components (HOC) |
| Component Type | Functional Components | Class or Wrapped Functional |
| Setup Complexity | Low (Direct import) | Medium (Provider/Wrapper setup) |
| Scoping | Built-in, component-local | Global map + Manual scope switching |
| Input Filtering | Automatic (enableOnFormTags) | Manual or Global Config |
| Cleanup | Automatic (via useEffect) | Handled by HOC lifecycle |
| Maintenance Status | Active, Modern | Legacy, Low Activity |
react-hotkeys-hook is the clear winner for modern development. It feels like a natural extension of React itself. By using hooks, it reduces boilerplate, ensures proper cleanup, and makes it incredibly easy to create scoped, context-aware shortcuts. It is the recommended choice for any new project starting today.
react-shortcuts represents an older approach to solving the same problem. While it introduced useful concepts like global shortcut mapping, its reliance on HOCs makes it feel cumbersome in a hooks-first world. It should only be considered if you are maintaining an existing application that already depends on it, where a refactor might be too costly.
Final Thought: In the React ecosystem, patterns evolve quickly. Moving from HOCs to Hooks isn't just a syntax change — it's a shift towards better composition and simpler logic. For keyboard shortcuts, react-hotkeys-hook embodies this modern approach, letting you focus on what the shortcuts do rather than how to wire them up.
Choose react-hotkeys-hook for new projects, especially those using functional components and React Hooks. It offers a declarative API, automatic event listener cleanup, and built-in support for scoping hotkeys to specific components or ignoring them when inputs are focused. It is the standard choice for modern React development due to its ease of use and active maintenance.
Choose react-shortcuts only if you are maintaining a legacy codebase that already relies on its Higher-Order Component (HOC) pattern and global shortcut mapping architecture. It is generally not recommended for new projects because it lacks native hook support, requires more boilerplate to integrate with modern functional components, and has seen significantly less activity compared to hook-based alternatives.
npm i react-hotkeys-hook
A React hook for using keyboard shortcuts in components in a declarative way.
import { useHotkeys } from 'react-hotkeys-hook'
export const ExampleComponent = () => {
const [count, setCount] = useState(0)
useHotkeys('ctrl+k', () => setCount(prevCount => prevCount + 1))
return (
<p>
Pressed {count} times.
</p>
)
}
Scopes allow you to group hotkeys together. You can use scopes to prevent hotkeys from colliding with each other.
const App = () => {
return (
<HotkeysProvider initiallyActiveScopes={['settings']}>
<ExampleComponent />
</HotkeysProvider>
)
}
export const ExampleComponent = () => {
const [count, setCount] = useState(0)
useHotkeys('ctrl+k', () => setCount(prevCount => prevCount + 1), { scopes: ['settings'] })
return (
<p>
Pressed {count} times.
</p>
)
}
You can change the active state of a scope using the disableScope, enableScope and toggleScope functions
returned by the useHotkeysContext() hook. Note that you have to have your app wrapped in a <HotkeysProvider> component.
const App = () => {
return (
<HotkeysProvider initiallyActiveScopes={['settings']}>
<ExampleComponent />
</HotkeysProvider>
)
}
export const ExampleComponent = () => {
const { toggleScope } = useHotkeysContext()
return (
<button onClick={() => toggleScope('settings')}>
Change scope active state
</button>
)
}
This will only trigger the hotkey if the component is focused.
export const ExampleComponent = () => {
const [count, setCount] = useState(0)
const ref = useHotkeys<HTMLParagraphElement>('ctrl+k', () => setCount(prevCount => prevCount + 1))
return (
<p tabIndex={-1} ref={ref}>
Pressed {count} times.
</p>
)
}
useHotkeys(keys: string | string[], callback: (event: KeyboardEvent, handler: HotkeysEvent) => void, options: Options = {}, deps: DependencyList = [])
| Parameter | Type | Required? | Default value | Description |
|---|---|---|---|---|
keys | string or string[] | required | - | set the hotkeys you want the hook to listen to. You can use single or multiple keys, modifier combinations, etc. This will either be a string or an array of strings. To separate multiple keys, use a comma. This split key value can be overridden with the splitKey option. |
callback | (event: KeyboardEvent, handler: HotkeysEvent) => void | required | - | This is the callback function that will be called when the hotkey is pressed. The callback will receive the browsers native KeyboardEvent and the libraries HotkeysEvent. |
options | Options | optional | {} | Object to modify the behavior of the hook. Default options are given below. |
dependencies | DependencyList | optional | [] | The given callback will always be memoised inside the hook. So if you reference any outside variables, you need to set them here for the callback to get updated (Much like useCallback works in React). |
All options are optional and have a default value which you can override to change the behavior of the hook.
| Option | Type | Default value | Description |
|---|---|---|---|
enabled | boolean or (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => boolean | true | This option determines whether the hotkey is active or not. It can take a boolean (for example a flag from a state outside) or a function which gets executed once the hotkey is pressed. If the function returns false the hotkey won't get executed and all browser events are prevented. |
enableOnFormTags | boolean or FormTags[] | false | By default, hotkeys are disabled when a form element is focused. This prevents accidental triggering while the user is typing. Set to true to enable on all form tags, or pass an array of specific tags (e.g. ['input', 'textarea', 'select']) |
enableOnContentEditable | boolean | false | Enable hotkeys on elements with the contentEditable attribute set to true |
splitKey | string | + | Character that joins keys within a combination (e.g. shift+a). Change this if you need to listen for the + key itself |
delimiter | string | , | Character that separates different hotkey combinations mapped to the same callback (e.g. ctrl+a, shift+b) |
scopes | string or string[] | * | With scopes you can group hotkeys together. The default scope is the wildcard * which matches all hotkeys. Use the <HotkeysProvider> component to change active scopes. |
keyup | boolean | false | Trigger the callback on the browser's keyup event |
keydown | boolean | true | Trigger the callback on the browser's keydown event. Set both keyup and keydown to true to trigger on both events |
preventDefault | boolean or (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => boolean | false | Prevent the browser's default behavior for the matched keystroke. Useful for overriding shortcuts like meta+s. Note: some browser shortcuts (e.g. meta+w) cannot be overridden |
description | string | undefined | Human-readable description of what the hotkey does. Useful for building help panels |
useKey | boolean | false | Listen to the produced character instead of the physical key code |
ignoreModifiers | boolean | false | Ignore modifier keys when matching hotkeys |
sequenceTimeoutMs | number | 1000 | Time window in milliseconds for sequential hotkeys |
The hooks call signature is very flexible. For example if you don't need to set any special options you can use the dependency array as your third parameter:
useHotkeys('ctrl+k', () => console.log(counter + 1), [counter])
isHotkeyPressed(keys: string | string[], splitKey?: string = ',')This function allows us to check if the user is currently pressing down a key.
import { isHotkeyPressed } from 'react-hotkeys-hook'
isHotkeyPressed('esc') // Returns true if Escape key is pressed down.
You can also check for multiple keys at the same time:
isHotkeyPressed(['esc', 'ctrl+s']) // Returns true if Escape or Ctrl+S are pressed down.
Open up an issue or pull request and participate.
Checkout this repo, run yarn or npm i and then run the test script to test the behavior of the hook.
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
git checkout -b feature/AmazingFeature)git commit -m 'Add some AmazingFeature')git push origin feature/AmazingFeature)Distributed under the MIT License. See LICENSE for more information.
Johannes Klauss - @JohannesKlauss - klauss.johannes@gmail.com
Project Link: https://github.com/JohannesKlauss/react-hotkeys-hook