# React Hotkeys Hook Documentation
> Complete documentation for react-hotkeys-hook - A React hook for handling keyboard shortcuts in a declarative way.
This file contains all documentation content in a single document following the llmstxt.org standard.
## Quick Start
Get up and running with keyboard shortcuts in under a minute.
### Install
**yarn**
yarn add react-hotkeys-hook
**npm**
npm i react-hotkeys-hook
**bun**
bun i react-hotkeys-hook
### Usage
```jsx
import { useHotkeys } from 'react-hotkeys-hook';
```
```jsx live
function MyComponent() {
const [count, setCount] = useState(0);
useHotkeys('a', () => setCount(count => count + 1));
return (
Pressed the 'a' key {count} times.
);
}
```
:::note TypeScript support out of the box
`react-hotkeys-hook` is written in TypeScript — no additional `@types` package needed.
:::
---
## Installation
### Via Package Manager
Install the package using your preferred package manager:
**yarn**
yarn add react-hotkeys-hook
**npm**
npm i react-hotkeys-hook
**bun**
bun i react-hotkeys-hook
```jsx
import { useHotkeys } from 'react-hotkeys-hook';
```
### Via CDN
You can import the ES Module directly from [unpkg.com](https://unpkg.com):
```js
import { useHotkeys } from 'https://unpkg.com/react-hotkeys-hook@latest/dist/react-hotkeys-hook.esm.js';
```
---
## Basic Usage
## useHotkeys
The `useHotkeys` hook lets you listen to keyboard shortcuts declaratively and run a callback when the user presses the specified key combination.
:::tip Interactive examples
Every code example on this page is editable — change the code and see the result immediately.
:::
### Import
```js
import { useHotkeys } from 'react-hotkeys-hook';
```
Or with `require`:
```js
const { useHotkeys } = require('react-hotkeys-hook')
// or
const useHotkeys = require('react-hotkeys-hook').useHotkeys
```
### Simple hotkey
The simplest use case is binding a single key to a callback:
```jsx live
function ExampleComponent() {
useHotkeys('a', () => alert('Key a was pressed'))
return (
Press the a key to see it work.
)
}
```
> Click inside the live preview area to focus it before pressing keys.
***
Inside the callback you can do anything. A common pattern is updating component state:
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0)
useHotkeys('b', () => setCount(prevCount => prevCount + 1))
return (
Pressed the 'b' key {count} times.
)
}
```
`useHotkeys` attaches the listener when the component mounts and removes it when the component unmounts.
***
### Multiple hotkeys per component
Since `useHotkeys` is a hook, you can call it multiple times in the same component:
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0)
useHotkeys('q', () => setCount(prevCount => prevCount + 1))
useHotkeys('w', () => setCount(prevCount => prevCount - 1))
return (
The count is {count}.
)
}
```
***
### Key combinations
You can listen to key combinations — multiple keys pressed together. For example, a Jira-like `Shift+C` shortcut to create a new ticket:
```jsx live
function CreateIssue() {
const [showIssueCreatorModal, setShowIssueCreatorModal] = useState(false)
useHotkeys('shift+c', () => setShowIssueCreatorModal(true))
return (
<>
{showIssueCreatorModal &&
MODAL CONTENT
}
{!showIssueCreatorModal &&
issue list
}
>
)
}
```
You are not limited to two keys. Combinations like `ctrl+shift+a+c` are also valid (though perhaps not the most user-friendly).
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0)
useHotkeys('ctrl+shift+a+c', () => setCount(prevCount => prevCount + 1))
return (
Pressed the 'ctrl+shift+a+c' key {count} times.
)
}
```
:::note The keys argument is case-insensitive
It does not matter whether you write `CTRL+S`, `Ctrl+s`, `ctrl+S`, or any other case variation. They all listen for the `ctrl` and `s` keys. Uppercase letters are treated as lowercase — to listen for an uppercase `S`, use `shift+s`.
:::
***
### Multiple bindings for one callback
You can bind several different key combinations to the same callback by separating them with a comma:
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0)
useHotkeys('ctrl+shift+a+c, c, shift+c', () => setCount(prevCount => prevCount + 1))
return (
Received the combination {count} times.
)
}
```
You can also pass an array as the first argument: `useHotkeys(['ctrl+shift+a+c', 'c', 'shift+c'], ...)`
:::info Global listeners by default
The `ctrl+shift+a+c` and `shift+c` combinations are used by multiple examples on this page. If you already pressed one of them, the example above may start with a count greater than zero. Hotkeys are attached globally unless you use the returned ref to scope them.
:::
***
### Sequential hotkeys
You can listen for keys pressed in sequence using the `>` character:
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0)
useHotkeys('g>h>i', () => setCount(prevCount => prevCount + 1))
return (
Received the combination {count} times.
)
}
```
In this example, the user must press `g`, then `h`, then `i` in order to trigger the callback.
##### Timeout
By default, `useHotkeys` allows **1 second** between each key in a sequence. If the user does not press the next key within that window, the sequence resets. You can change this with the `sequenceTimeoutMs` option:
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0)
useHotkeys('g>h>i>j', () => setCount(prevCount => prevCount + 1), {
sequenceTimeoutMs: 10000,
})
return (
Received the combination {count} times.
)
}
```
Here the user has 10 seconds to complete the `g>h>i>j` sequence.
***
### Modifiers
`useHotkeys` supports the following modifier keys:
* `shift`
* `alt`
* `ctrl`
* `meta`
* `mod` — triggers on **either** `ctrl` or `meta`, regardless of platform. This is commonly used as `ctrl` on Windows/Linux and `cmd` on macOS.
:::info `mod` listens to both modifier keys
`mod` is an alias that triggers if **either** `ctrl` or `meta` is pressed. If you need to listen to the platform's primary modifier **exclusively** (e.g. only `cmd` on macOS or only `ctrl` on Windows), use `ctrl` or `meta` directly instead of `mod`.
:::
### Cross-platform compatibility
Since version 4, `alt` and `option` are treated as identical. The `meta` key is `cmd` on macOS and the Windows/`os` key on Windows/Linux.
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0)
useHotkeys(
'ctrl+shift+a+c, c, shift+c, alt+n, ctrl+d, meta+d',
() => setCount(prevCount => prevCount + 1)
)
return (
Received the combination {count} times.
)
}
```
***
### Special keys
Arrow keys, return, space, and other special keys have dedicated names:
* `backspace`
* `tab`
* `clear`
* `enter` or `return`
* `esc` or `escape`
* `space`
* `up`, `down`, `left`, `right`
* `pageup`, `pagedown`
* `del` or `delete`
* `f1`, `f2` ... `f19`
:::caution Browsers reserve some shortcuts
Some shortcuts cannot be overridden because they are essential to the browser's safe browsing experience. In Chrome, for example:
* `meta+w` — close tab
* `meta+n` — new window
* `meta+t` — new tab
* `meta+shift+w` — close all tabs of the current window
* `meta+shift+n` — open incognito window
* `meta+shift+t` — reopen last closed tab
* `meta+1..9` — focus corresponding tab
:::
---
## Disable/Enable Hotkeys
So far we have covered registering hotkeys, scoping them to components, and preventing stale state. But hotkeys are active by default. Sometimes you need to control whether a hotkey should be enabled at all.
There are several ways to disable a hotkey. Let's go through them.
### Dynamically disable or enable
#### Toggle hotkeys with the `enabled` flag
Use the `enabled` option to control whether a hotkey is active:
```jsx live
function ExampleComponent() {
const [enabled, setEnabled] = useState(false)
const [count, setCount] = useState(0)
useHotkeys('b', () => setCount(prevCount => prevCount + 1), {
enabled,
})
return (
Hotkey is {!enabled && 'not'} enabled.
Pressed the 'b' key {count} times.
)
}
```
This is useful when you want to enable or disable a hotkey based on application state or user input. `enabled` accepts a `Trigger` type — either a boolean or a function that returns a boolean. See the [`enabled` API docs](/docs/api/use-hotkeys#enabled) for details.
:::info Automatic scoping
If you use the returned `ref` to scope a hotkey to a specific element, the hotkey automatically activates only when that element (or one of its children) has focus.
:::
***
### Prevent default browser behavior
Some shortcuts like `meta+s` are reserved by the browser — in this case, to save the current page. If you are building a text editor or any creation tool, you probably want to override `meta+s` with your own save logic.
Set `preventDefault` to `true` to stop the browser from handling the shortcut:
```jsx live
function ExampleComponent() {
useHotkeys('meta+s', () => {
alert('We saved your progress!')
// ... your own save logic
}, { preventDefault: true })
return (
Press cmd+s (or ctrl+s on non-Mac) to open the custom save dialog.
)
}
```
:::caution Browsers reserve some shortcuts
Some shortcuts cannot be overridden because they are essential to the browser's safe browsing experience. In Chrome, for example:
* `meta+w` — close tab
* `meta+n` — new window
* `meta+t` — new tab
* `meta+shift+w` — close all tabs of the current window
* `meta+shift+n` — open incognito window
* `meta+shift+t` — reopen last closed tab
* `meta+1..9` — focus corresponding tab
:::
***
#### Disable callback execution while keeping listeners
Setting `enabled` to `false` removes the event listeners from the DOM entirely. If you want to keep the listeners active but skip the callback, pass a function that returns `false`:
```jsx live
function ExampleComponent() {
const [blockHotkey, setBlockHotkey] = useState(false)
useHotkeys('b', () => alert('Callback executed'), {
enabled: () => !blockHotkey,
})
return (
When blocked, the 'b' key does nothing. When unblocked, it triggers the alert.
)
}
```
You can combine `enabled` and `preventDefault`:
```jsx
function ExampleComponent() {
useHotkeys('meta+s', () => alert('We saved your progress!'), {
enabled: () => false,
preventDefault: true,
})
return (
Press cmd+s (or ctrl+s on non-Mac) to open the custom save dialog.
)
}
```
:::info Different behavior for `enabled: false` vs `enabled: () => false`
* `enabled: false` — the browser event is not captured at all. No browser behavior happens.
* `enabled: () => false` — the event is still captured, so potential browser behavior still occurs, but your callback is skipped.
:::
***
#### Enable hotkeys on form fields
By default, hotkeys do not trigger when the user is focused on a form field. This prevents accidents like typing the letter `p` into a review textarea and triggering a "print" hotkey.
However, some shortcuts — like `meta+s` for saving — should still work while the user is typing. Use the `enableOnFormTags` option to allow specific form tags:
```jsx
function ExampleComponent() {
useHotkeys('meta+s', (e) => {
e.preventDefault()
alert('We saved your progress!')
// ... your own save logic
}, {
enableOnFormTags: ['input', 'select', 'textarea']
})
return (
Press cmd+s (or ctrl+s on non-Mac) to open the custom save dialog.
)
}
```
Pass `true` to enable the hotkey on **all** form tags.
***
#### Enable hotkeys on `contentEditable` elements
The [`contentEditable`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable) attribute lets users edit text inside any element — like a `
`. By default, hotkeys are disabled on these elements too. To enable them, use the `enableOnContentEditable` option:
```jsx live
function ExampleComponent() {
useHotkeys('meta+b', () => alert('Hotkeys are still working'), {
enableOnContentEditable: true,
})
return (
You can edit this text and still use all active shortcuts.
)
}
```
---
## Keyboard Layouts
## Working with Keyboard Layouts
Keyboard shortcuts behave differently depending on the user's keyboard layout. By default, `react-hotkeys-hook` listens to the physical key code — not the character produced by that key.
If you want to listen for a special character like `!`, `:`, or `%`, you have two approaches.
### Option 1 — Listen to the physical key (layout-independent)
This is the default behavior. The hook listens to which physical keys are pressed, not the resulting character. For example, to respond to the `!` character on a US layout, you listen to the physical keys that produce it:
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0)
useHotkeys('shift+1', () => setCount(prevCount => prevCount + 1))
return (
Pressed the '!' key {count} times.
)
}
```
This is layout-independent: the same physical keystroke (`shift+1`) works for all users, even if their layout produces a different character. The trade-off is that you must communicate the physical shortcut (`shift+1`) to users, not the character (`!`), because you cannot guarantee that `shift+1` produces `!` on every layout.
### Option 2 — Listen to the produced character (layout-dependent)
If you care about the character itself rather than how it is typed, use the `useKey` option. This listens to the produced key instead of the physical key code:
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0)
useHotkeys('!', () => setCount(prevCount => prevCount + 1), { useKey: true })
return (
Pressed the '!' key {count} times.
)
}
```
Now the hotkey only triggers when the user produces the `!` character, regardless of which physical keys they press. On a standard US keyboard, `shift+1` and `!` are equivalent, but on other layouts they may differ. Choose `useKey: true` when the character matters more than the physical keystroke.
### Symbol and punctuation keys
Shortcuts that involve symbol or punctuation keys like `=`, `+`, `;`, `,`, `?`, or `/` are a common source of confusion. The same two approaches apply, but the string you pass to `useHotkeys` looks different depending on which one you choose.
#### Listening to the physical key
Pass the physical key's [`KeyboardEvent.code`](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values) name. These names are derived from the US layout and stay the same regardless of which layout the user actually uses:
| Character on US layout | Physical key code |
|------------------------|-------------------|
| `-` | `Minus` |
| `=` | `Equal` |
| `[` | `BracketLeft` |
| `]` | `BracketRight` |
| `\` | `Backslash` |
| `;` | `Semicolon` |
| `'` | `Quote` |
| `,` | `Comma` |
| `.` | `Period` |
| `/` | `Slash` |
| `` ` `` | `Backquote` |
For example, to listen to the physical `;` key — regardless of layout — pass the code name:
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0)
useHotkeys('Semicolon', () => setCount(prevCount => prevCount + 1))
return (
Pressed the ';' key {count} times.
)
}
```
The code names are case-insensitive, so `Semicolon` and `semicolon` behave the same. The same applies when you combine them with modifiers:
```js
useHotkeys('ctrl+Equal', zoomIn, { preventDefault: true })
useHotkeys('ctrl+Minus', zoomOut, { preventDefault: true })
useHotkeys('mod+Slash', toggleComment)
```
#### Listening to the produced character
If you care about the character itself — for example, you want `?` to open a help dialog whether the user is on a US layout (`shift+/`) or a German layout (`shift+ß`) — set `useKey: true` and pass the character directly:
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0)
useHotkeys('?', () => setCount(prevCount => prevCount + 1), { useKey: true })
return (
Pressed '?' {count} times.
)
}
```
The same works for `=`, `+`, `/`, `;`, and most other printable characters:
```js
useHotkeys('+', addItem, { useKey: true })
useHotkeys('=', resetZoom, { useKey: true })
useHotkeys('/', focusSearch, { useKey: true })
```
:::caution Watch out for `+` and `,`
By default, `+` is the [`splitKey`](/docs/api/use-hotkeys#splitkey) that joins keys in a combination, and `,` is the [`delimiter`](/docs/api/use-hotkeys#delimiter) that separates multiple hotkeys. To listen for those characters themselves, either switch to the physical-key approach (`Equal`/`Comma`) or change the option:
```js
// Listen to the '+' character by changing the splitKey
useHotkeys('ctrl-+', addItem, { splitKey: '-' })
```
:::
#### When to choose which
| You want… | Use |
|--------------------------------------------------------------------------------------------------------|--------------------------------------|
| Editor-style shortcuts that always work on the same physical key (e.g. `Ctrl+;` for "go to file") | Physical key (`'ctrl+Semicolon'`) |
| A user-visible character that appears in your UI hint (e.g. "Press `?` for help") | `useKey: true` |
| The numpad `+` instead of the main-row `+` | Physical key (`'NumpadAdd'`) |
:::note Why two approaches?
On a US keyboard, `?` is produced by `Shift + Slash`. On a German keyboard, the same physical `Slash` key produces `-`, and `?` is produced elsewhere. Listening to the physical key gives you a stable position across layouts; listening to the produced character gives you a stable user-visible symbol.
:::
---
## Scoping Hotkeys
## Scoping hotkeys to components
### Global hotkeys and scoped components
In the previous section we used two keystrokes in two different examples (`ctrl+shift+a+c` and `shift+c`).
If you press one of those keystrokes, both examples trigger. Why?
By default, hotkeys are attached globally — there is no built-in scoping mechanism that limits a hotkey to only trigger when its component is focused. Here are two components that both listen for the `c` key:
```jsx live noInline
function UnscopedHotkey() {
const [count, setCount] = useState(0)
useHotkeys('c', () => setCount(prevCount => prevCount + 1))
return (
)
```
Every time you press the `c` key, both counters increment. To isolate a hotkey to a specific component, `useHotkeys` returns a [React ref callback](https://react.dev/reference/react-dom/components/common#ref-callback) that you attach to the element that should have focus before the hotkey fires.
```jsx live noInline
function ScopedHotkey() {
const [count, setCount] = useState(0)
const ref = useHotkeys('shift+a', () => setCount(prevCount => prevCount + 1))
return (
<>
The count is {count}. Click anywhere except the button to disable the hotkey.
The count is {count}. Click anywhere except the button to disable the hotkey.
>
)
}
render(
)
```
Now each hotkey only fires when its assigned button has focus. You can use this technique for any hotkey, not just duplicates.
***
### Scoping with non-focusable elements
Relying on a button for focus is rarely useful in real applications. More often, you want a container — like a modal or a card — to receive focus and enable its hotkeys. Elements like `
`, ``, and `` cannot receive focus by default. To make them focusable, add a `tabIndex` attribute:
```jsx live
function ScopedHotkey() {
const [count, setCount] = useState(0)
const ref = useHotkeys('shift+a', () => setCount(prevCount => prevCount + 1))
return (
The count is {count}. Click inside this area to enable the hotkey.
)
}
```
:::info Use `tabIndex={-1}`
Setting `tabIndex` to `-1` makes an element focusable via JavaScript or mouse click, but keeps it out of the keyboard tab order. This prevents accidental disruption of the page's natural tab flow. For more details, see the [MDN page on tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex).
:::
***
### Nesting scoped hotkeys
Scoped hotkeys also work with nested components:
```jsx live noInline
function NestedScopedHotkey() {
const [count, setCount] = useState(0)
const ref = useHotkeys('shift+a', () => setCount(prevCount => prevCount + 1))
return (
The count is {count}. Click inside this area to enable the hotkey.
The count is {count}. Click inside this area to enable the hotkey.
{children}
)
}
render(
)
```
When nested scopes share the same hotkey, the innermost focused element takes precedence.
---
## Callback Dependencies
## Setting callback dependencies
The callback you pass to `useHotkeys` is memoized automatically inside the hook to prevent unnecessary re-renders.
This can lead to [stale state](https://react.dev/reference/react/useState#im-trying-to-set-state-to-a-function-but-it-gets-called-instead) — the callback captures old values from its closure.
To prevent this, `useHotkeys` accepts a dependency array just like React's built-in
[`useEffect`](https://react.dev/reference/react/useEffect), [`useMemo`](https://react.dev/reference/react/useMemo), and
[`useCallback`](https://react.dev/reference/react/useCallback) hooks.
> The general rule: put every unstable reference used inside the callback into the dependency array.
Let's look at a simple counter that works correctly because it uses the functional updater form of `setCount`:
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0)
const ref = useHotkeys('b', () => setCount(prevCount => prevCount + 1))
return (
Pressed the 'b' key {count} times.
)
}
```
:::note Scoped to this component
We use the returned `ref` to scope the hotkey to this specific element. The `tabIndex={-1}` prop lets the `` receive focus, since span elements are not focusable by default.
:::
***
### The stale-state problem
Now let's change the callback to reference `count` directly:
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0)
const ref = useHotkeys('b', () => setCount(count + 1))
return (
Pressed the 'b' key {count} times.
)
}
```
The counter stops at `1`. This is **stale state**: the callback was memoized during the first render when `count` was `0`, so `count + 1` is always `1`.
The functional updater form (`prevCount => prevCount + 1`) avoids this because it receives the latest state value. But that solution only works for `useState`. What if you need to reference a `useMemo` result, a prop, or some other external value?
### Using the dependency array
Add any value that changes over time to the dependency array:
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0)
const squaredCount = useMemo(() => count * 2, [count])
const ref = useHotkeys('b', () => setCount(squaredCount + 1), [squaredCount])
return (
Pressed the 'b' key {squaredCount} times.
)
}
```
If you remove `[squaredCount]` from `useHotkeys`, the callback appears to stop working after the first keypress. In reality, the callback still runs — it just uses the stale value of `squaredCount` from the first render.
---
## Advanced Usage
### Using `ts-key-enum` for type-safe key names
It can be hard to remember whether the left arrow key is called `ArrowLeft` or `LeftArrow`. The `ts-key-enum` library provides TypeScript enums for all key names, giving you autocomplete and eliminating guesswork.
Install it:
```bash
npm install ts-key-enum
```
Use it in your code:
```ts
import { Key } from 'ts-key-enum'
useHotkeys(Key.Backspace, () => {
console.log('Delete pressed')
})
```
See the [full list of available keys](https://gitlab.com/nfriend/ts-key-enum/-/blob/master/Key.enum.d.ts).
***
### Building a help panel with metadata
You can attach a `description` and custom `metadata` to every hotkey. This makes it easy to build a dynamic help panel that lists all active shortcuts:
```tsx
function useActiveHotkeys() {
// You would collect this from your components or a registry
const hotkeys = [
{ keys: 'ctrl+s', description: 'Save document', scope: 'editor' },
{ keys: 'ctrl+z', description: 'Undo', scope: 'editor' },
{ keys: 'esc', description: 'Close modal', scope: 'modal' },
];
return hotkeys;
}
function HelpPanel() {
const hotkeys = useActiveHotkeys();
return (
Keyboard Shortcuts
{hotkeys.map(h => (
{h.keys} — {h.description}
))}
);
}
```
When registering hotkeys, include the `description` option:
```tsx
useHotkeys('ctrl+s', handleSave, {
description: 'Save the current document',
metadata: { category: 'file' },
});
```
***
### Custom hooks built on `useHotkeys`
Wrapping `useHotkeys` in a custom hook keeps your components clean and enforces consistent behavior across your app:
```tsx
function useEditorShortcuts() {
const { enableScope, disableScope } = useHotkeysContext();
useHotkeys('ctrl+s', saveDocument, {
scopes: 'editor',
preventDefault: true,
description: 'Save document',
});
useHotkeys('ctrl+z', undo, {
scopes: 'editor',
preventDefault: true,
description: 'Undo',
});
useHotkeys('ctrl+shift+z', redo, {
scopes: 'editor',
preventDefault: true,
description: 'Redo',
});
return { enableScope, disableScope };
}
```
Then in your editor component:
```tsx
function Editor() {
useEditorShortcuts();
return
{/* editor content */}
;
}
```
***
### Listening inside iframes
If your React app runs inside an iframe, pass the iframe's `document` object to bind hotkeys to that document instead of the main page:
```tsx
import FrameComponent from 'react-frame-component'
function InsideFrameComponent() {
const { document } = useFrame()
useHotkeys('s', () => console.log('Triggered inside iframe'), { document })
return
....
}
function App() {
return (
)
}
```
***
### Event listener options
You can pass custom [event listener options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options) to the underlying `addEventListener` call:
```tsx
useHotkeys('ctrl+s', handleSave, {
eventListenerOptions: { passive: true, capture: true },
});
```
This is useful when you need fine-grained control over event propagation.
---
## Grouping Hotkeys together
## `HotkeysProvider`
`HotkeysProvider` lets you group hotkeys into named scopes. This is useful when you want to disable or enable a whole group of hotkeys at once — for example, when opening a modal or switching between different application modes.
Wrap your application (or the relevant part of it) with `HotkeysProvider`:
```jsx
import { HotkeysProvider } from 'react-hotkeys-hook';
function App() {
return (
My App
)
}
```
### Assigning hotkeys to scopes
Use the `scopes` option to assign a hotkey to one or more scopes:
```jsx
import { useHotkeys } from 'react-hotkeys-hook';
function MyComponent() {
useHotkeys('ctrl+a', () => console.log('ctrl+a'), { scopes: 'scopeA' })
useHotkeys('ctrl+b', () => console.log('ctrl+b'), { scopes: 'scopeB' })
useHotkeys('ctrl+c', () => console.log('ctrl+c'), { scopes: 'scopeA' })
return (
My App
)
}
```
By default, all hotkeys are in the `*` (wildcard) scope. This means they are active regardless of which scopes are currently enabled. Wrapping your app with `HotkeysProvider` enables the wildcard scope automatically.
If you assign a hotkey to a specific scope, it **leaves** the wildcard scope. It will only fire when that scope is active.
### Activating and deactivating scopes
Use the `useHotkeysContext` hook to control scopes programmatically:
```jsx
import { useHotkeysContext } from 'react-hotkeys-hook';
function MyComponent() {
const { enableScope, disableScope, toggleScope } = useHotkeysContext();
return (
)
}
```
:::tip Check active scopes
If your application has dynamic scope switching, you can read `activeScopes` from the context to see which scopes are currently enabled.
:::
### Set initially active scopes
Pass an array of scope names to the `initiallyActiveScopes` prop:
```jsx
import { HotkeysProvider } from 'react-hotkeys-hook';
function App() {
return (
My App
)
}
```
:::info Wildcard scope and `initiallyActiveScopes`
When you set `initiallyActiveScopes`, the wildcard scope `*` is **not** active by default. To include it, explicitly add it to the array: `['scopeA', '*']`.
:::
### Common use case: modal dialogs
A typical pattern is to disable application-level hotkeys when a modal opens, and enable a separate set of modal-specific hotkeys:
```jsx
function App() {
return (
)
}
function Editor() {
useHotkeys('ctrl+s', saveDocument, { scopes: 'editor' })
// ...
}
function SettingsModal() {
const { enableScope, disableScope } = useHotkeysContext()
useEffect(() => {
enableScope('settings')
disableScope('editor')
return () => {
disableScope('settings')
enableScope('editor')
}
}, [enableScope, disableScope])
useHotkeys('esc', closeModal, { scopes: 'settings' })
return
{/* modal content */}
}
```
---
## isHotkeyPressed
`isHotkeyPressed` lets you check whether a specific key or key combination is currently being held down. This is useful when you need to know modifier state inside another event handler — for example, inside a `click` handler.
### Basic usage
Import `isHotkeyPressed` from the package:
```js
import { isHotkeyPressed } from 'react-hotkeys-hook';
```
#### Check for pressed keys inside callbacks
A common use case is changing behavior based on whether a modifier key is held:
```jsx live
function ExampleComponent() {
const [count, setCount] = useState(0);
const onClick = () => isHotkeyPressed('shift')
? setCount(count => count - 1)
: setCount(count => count + 1);
return (
The count is: {count}
Hold Shift while clicking to decrease.
)
}
```
#### Check multiple keys simultaneously
Pass an array or a comma-delimited string to check if multiple keys are pressed at the same time:
```jsx live
function ExampleComponent() {
const [pressed, setPressed] = useState(false);
const onClick = () => setPressed(isHotkeyPressed(['shift', 'a']));
return (
The hotkey was: {pressed ? 'pressed' : 'not pressed'}
)
}
```
All keys in the array must be pressed simultaneously for `isHotkeyPressed` to return `true`.
---
## Using TypeScript
:::tip Works out of the box!
React Hotkeys Hook is written in TypeScript and works seamlessly with TypeScript projects. No additional `@types` package is needed.
:::
### Type-safe key names
The `keys` argument must be a string or array of strings. TypeScript catches incorrect types at compile time:
```tsx
import { useHotkeys } from "react-hotkeys-hook";
import { useState } from "react";
interface Props {
hotKey: number; // ❌ wrong type
}
const MyComponent = ({ hotKey }: Props) => {
const [count, setCount] = useState(0);
// TypeScript error: hotKey must be a string
useHotkeys(hotKey, () => setCount(prevCount => prevCount + 1));
return (
The count is {count}.
)
}
```
### Typing the ref
When scoping hotkeys to a specific element, you can type the returned ref for full type safety:
```tsx
import { useHotkeys } from "react-hotkeys-hook";
import { useState } from "react";
interface Props {
hotKey: string;
}
const MyComponent = ({ hotKey }: Props) => {
const [count, setCount] = useState(0);
const ref = useHotkeys(
hotKey,
() => setCount(prevCount => prevCount + 1)
);
return (
The count is {count}.
)
}
```
### Typed options
The `options` object is fully typed, so you get autocomplete and type checking for all configuration properties:
```tsx
useHotkeys('ctrl+s', handleSave, {
enabled: true,
preventDefault: true,
scopes: ['editor'],
enableOnFormTags: ['input', 'textarea'],
});
```
### Custom metadata
You can attach custom metadata to hotkeys for your own use cases (for example, building a help panel that lists all active shortcuts):
```tsx
useHotkeys('ctrl+s', handleSave, {
description: 'Save the current document',
metadata: { category: 'file', showInHelp: true },
});
```
---
## useRecordHotkeys
`useRecordHotkeys` lets you capture whatever keys the user presses. This is useful for building custom keyboard shortcut settings — for example, letting users define their own hotkeys.
### Basic usage
Import the hook from the package:
```js
import { useRecordHotkeys } from 'react-hotkeys-hook';
```
#### Record hotkeys
Call `useRecordHotkeys` to start capturing keystrokes. It returns a tuple: the first element is a `Set` of recorded keys, and the second element is a control object with `start`, `stop`, `resetKeys`, and `isRecording`.
```jsx live
function ExampleComponent() {
const [keys, { start, stop, isRecording }] = useRecordHotkeys()
return (
Is recording: {isRecording ? 'yes' : 'no'}
Recorded keys: {Array.from(keys).join(' + ')}
)
}
```
:::info Recorded keys are a Set
Each key is recorded only once. The returned value is a `Set` rather than an array, which makes deduplication easy. Convert it to an array when you need to display or store the keys.
:::
### Ignoring specific keys
Pass a `blacklist` array as the second argument to exclude certain keys from being recorded. Blacklisted keys keep their default behavior and are not added to the recorded set.
```jsx live
function ExampleWithBlacklist() {
const [keys, { start, stop, resetKeys, isRecording }] = useRecordHotkeys(false, ['tab', 'enter']);
return (
Recording (ignoring Tab and Enter): {isRecording ? 'yes' : 'no'}
Recorded keys: {Array.from(keys).join(' + ')}
);
}
```
### Recording produced characters vs. key codes
By default, `useRecordHotkeys` records key codes (physical keys). To record the produced character instead, pass `true` as the first argument:
```jsx
const [keys, controls] = useRecordHotkeys(true); // records characters like "!" instead of "shift+1"
```
---
## isHotkeyPressed(Api)
## isHotkeyPressed API
Function signature:
```ts
function isHotkeyPressed(key: string | string[], delimiter: string = ','): boolean
```
Check if a specific key or combination of keys is currently pressed.
```jsx
import { isHotkeyPressed } from 'react-hotkeys-hook';
const onClick = () => isHotkeyPressed('shift') ? setCount(count => count - 1) : setCount(count => count + 1);
```
***
### Arguments
#### `key`
```ts
key: string | string[]
```
The key or keys to check. This can be:
* A single keycode string like `'a'`
* A delimited string of keycodes like `'shift,a'`
* An array of keycode strings like `['shift', 'a']`
When multiple keys are provided, **all** of them must be pressed down for the function to return `true`.
:::caution `mod` is not supported
Use `isHotkeyPressed(['ctrl', 'a'])` or `isHotkeyPressed(['meta', 'a'])` instead of `mod`.
:::
#### `delimiter`
```ts
delimiter: string // default: ","
```
The delimiter used to parse multiple keys from a single string.
### Return value
#### `boolean`
Returns `true` if the key or key combination is currently pressed, otherwise `false`.
---
## useHotkeys
## useHotkeys API
Function signature:
```ts
function useHotkeys(
keys: string | string[],
callback: (event: KeyboardEvent, handler: HotkeysEvent) => void,
options: Options = {},
deps: any[] = []
): React.RefCallback
```
***
### Arguments
#### `keys`
```ts
keys: string | string[]
```
The keystrokes the hook should listen for. Supports single keys, modifier combinations, arrow keys, function keys, and more.
##### Listen to all keys
```jsx
useHotkeys('*', (_, handler) => alert(handler.key))
```
##### Use modifiers
```jsx
useHotkeys('ctrl+s, shift+w', () => alert('We're using modifiers now!'))
```
##### Use function keys
```jsx
useHotkeys('f5', () => alert('F5 was pressed'))
```
##### Multiple key combinations
```jsx
useHotkeys('w, a, s, d', () => alert('Player moved!'))
```
:::tip Differentiate between multiple keys
When multiple possible keys share the same callback, use `handler.keys` to determine which one was pressed:
```jsx
useHotkeys('ctrl+a, shift+b, r, f', (_, handler) => {
switch (handler.keys.join('')) {
case 'a': alert('You pressed ctrl+a!');
break;
case 'b': alert('You pressed shift+b!');
break;
case 'r': alert('You pressed r!');
break;
case 'f': alert('You pressed f!');
break;
}
})
```
:::
You can also pass an array:
```jsx
useHotkeys(['ctrl+a', 'shift+b', 'r', 'f'], (_, handler) => {
switch (handler.keys.join('')) {
case 'a': alert('You pressed ctrl+a!');
break;
case 'b': alert('You pressed shift+b!');
break;
case 'r': alert('You pressed r!');
break;
case 'f': alert('You pressed f!');
break;
}
})
```
Or work directly with the matched hotkey string:
```jsx
const HOTKEYS = {
ACTION_A: 'ctrl+a',
ACTION_B: 'shift+b',
}
useHotkeys(Object.values(HOTKEYS), (_, { hotkey }) => {
switch (hotkey) {
case HOTKEYS.ACTION_A: alert('You pressed ctrl+a!');
break;
case HOTKEYS.ACTION_B: alert('You pressed shift+b!');
break;
}
})
```
***
#### `callback`
```ts
callback: (event: KeyboardEvent, handler: HotkeysEvent) => void
```
Called when the user presses the defined keystroke. `event` is the browser's native `KeyboardEvent`. `handler` contains additional information about the pressed key.
##### `event`
The browser's native keyboard event. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent) for full documentation.
##### `handler`
The `handler` object provides information about the matched hotkey:
* `keys: string[]` — The keys that were pressed. Useful when multiple key combinations map to the same callback.
* `hotkey: string` — The full hotkey string that matched (e.g. `'ctrl+a'`).
* `scopes?: string | string[]` — The scope(s) assigned to this hotkey.
* `description?: string` — The description assigned via options.
* `metadata?: Record` — Custom metadata assigned via options.
:::info The callback is memoized
The callback is memoized internally, so any variable referenced inside it must be listed in the dependencies array. Otherwise you will get stale values. For more on memoization in React hooks, see [this article](https://medium.com/@sdolidze/react-hooks-memoization-99a9a91c8853).
:::
***
#### `options`
Configure the hook's behavior by passing an options object.
```js
// Default values
const options = {
enabled: true,
enableOnFormTags: false,
enableOnContentEditable: false,
splitKey: '+',
delimiter: ',',
scopes: '*',
keyup: undefined,
keydown: true,
preventDefault: false,
description: undefined,
document: undefined,
ignoreModifiers: false,
useKey: false,
sequenceTimeoutMs: 1000,
sequenceSplitKey: '>',
eventListenerOptions: undefined,
metadata: undefined,
};
```
```ts
// Type Definitions
type Trigger = boolean | ((keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => boolean)
type FormTags = 'input' | 'textarea' | 'select' | 'INPUT' | 'TEXTAREA' | 'SELECT' | 'searchbox' | 'slider' | 'spinbutton' | 'menuitem' | 'menuitemcheckbox' | 'menuitemradio' | 'option' | 'radio' | 'textbox';
type Options = {
enabled?: Trigger
enableOnFormTags?: FormTags[] | boolean
enableOnContentEditable?: boolean
splitKey?: string
delimiter?: string
scopes?: string | string[]
keyup?: boolean
keydown?: boolean
preventDefault?: Trigger
description?: string
document?: Document
ignoreModifiers?: boolean
useKey?: boolean
sequenceTimeoutMs?: number
sequenceSplitKey?: string
eventListenerOptions?: EventListenerOptions
metadata?: Record
};
```
##### `enabled`
```ts
enabled: Trigger // default: true
```
Determines if the callback should be triggered. Pass `false` to disable the hotkey entirely, or a function that receives the keyboard event and returns `true` or `false`.
When `enabled` is a boolean `false`, the event listeners are removed from the DOM. When `enabled` is a function that returns `false`, the listeners stay attached but the callback is skipped.
##### `enableOnFormTags`
```ts
enableOnFormTags: FormTags[] | boolean // default: false
```
By default, hotkeys are disabled while the user is focused on a form element. Set this to `true` to enable hotkeys on all form tags, or pass an array of specific tags:
```js
useHotkeys('ctrl+s', save, { enableOnFormTags: ['input', 'textarea', 'select'] })
```
##### `enableOnContentEditable`
```ts
enableOnContentEditable: boolean // default: false
```
Enable hotkeys on elements with the `contentEditable` attribute.
##### `ignoreEventWhen`
```ts
ignoreEventWhen: (e: KeyboardEvent) => boolean // default: undefined
```
Fine-grained control over which events to ignore. Return `true` from the function to skip handling the event:
```js
useHotkeys('a', someCallback, {
ignoreEventWhen: (e) => {
return e.target.className.includes('special-element')
},
})
```
##### `splitKey`
```ts
splitKey: string // default: "+"
```
The character that joins keys within a single combination. The default is `+`, so `shift+a` triggers when the user presses Shift **and** A together.
Change this if you need to listen for the `+` key itself:
```js
useHotkeys('ctrl-+', zoomIn, { splitKey: '-' })
```
##### `delimiter`
```ts
delimiter: string // default: ","
```
The character that separates different hotkey combinations mapped to the same callback. The default is `,`, so `ctrl+a, shift+b` listens for either combination.
##### `sequenceSplitKey`
```ts
sequenceSplitKey: string // default: ">"
```
The character that separates keys in a sequential hotkey. The default is `>`, so `g>h>i` requires pressing G, then H, then I in sequence.
##### `scopes`
```ts
scopes: string | string[] // default: "*"
```
Assign the hotkey to one or more scopes. Scopes let you group hotkeys and enable or disable them together via `HotkeysProvider`. See the [Grouping Hotkeys](/docs/documentation/hotkeys-provider) documentation for details.
##### `keyup`
```ts
keyup: boolean // default: false
```
Trigger the callback on the browser's `keyup` event.
##### `keydown`
```ts
keydown: boolean // default: true
```
Trigger the callback on the browser's `keydown` event. This is the default.
:::tip Listening to both `keydown` and `keyup`
If you set `keyup: true` without explicitly setting `keydown`, the hook assumes you only want `keyup`. To listen to both, set both options:
```js
useHotkeys('a', callback, {
keydown: true,
keyup: true
})
```
:::
##### `preventDefault`
```ts
preventDefault: Trigger // default: false
```
Prevent the browser's default behavior for the matched keystroke. Useful for overriding shortcuts like `meta+s` (save page).
```js
useHotkeys('meta+s', someCallback, {
preventDefault: true,
});
```
##### `description`
```ts
description: string // default: undefined
```
A human-readable description of what the hotkey does. Useful for building help panels or shortcut reference lists.
##### `document`
```ts
document: Document // default: undefined
```
Bind the event listeners to a specific `Document` object instead of the global `document`. Useful for apps running inside iframes.
```js
import FrameComponent from 'react-frame-component'
const InsideFrameComponent = () => {
const { document } = useFrame()
useHotkeys("s", () => console.log("Triggered inside iframe"), { document })
return
....
}
```
##### `ignoreModifiers`
```ts
ignoreModifiers: boolean // default: false
```
Ignore modifier keys (`shift`, `alt`, `ctrl`, `meta`) when matching hotkeys. Useful when you want to listen for a character regardless of how it is typed — for example, listening for `/` to focus a search field, whether the user presses the dedicated `/` key or `shift+7` on some layouts.
```tsx
function App() {
useHotkeys('/', () => inputRef.current?.focus(), { ignoreModifiers: true, preventDefault: true })
const inputRef = useRef(null)
return (
)
}
```
:::info Why is this necessary?
Keyboard layouts vary across languages and operating systems. For example, `#` is typed with `Shift+3` on a US keyboard but has its own dedicated key on a German keyboard. `[` is `Option+5` on a German macOS layout, but a dedicated key on Windows.
Users can also customize their layouts. So listening to `#` may or may not involve a shift modifier.
With `ignoreModifiers: true`, you say: "I want the `#` character, however the user produces it." Without it, you say: "I want the `Shift+3` physical keystroke, regardless of what character it produces."
:::
##### `useKey`
```ts
useKey: boolean // default: false
```
Listen to the produced character instead of the physical key code. Useful when you want to match a specific symbol (like `!` or `?`) regardless of which physical keys produce it on the user's layout.
##### `sequenceTimeoutMs`
```ts
sequenceTimeoutMs: number // default: 1000
```
The time window in milliseconds for sequential hotkeys. If the user does not press the next key within this window, the sequence resets.
##### `eventListenerOptions`
```ts
eventListenerOptions: EventListenerOptions // default: undefined
```
Pass custom options to the underlying `addEventListener` call, such as `{ passive: true }` or `{ capture: true }`.
##### `metadata`
```ts
metadata: Record // default: undefined
```
Attach arbitrary custom data to the hotkey. You can retrieve it from the `handler` object in the callback (`handler.metadata`). Useful for building dynamic shortcut registries or help panels.
***
#### `deps`
```ts
deps: any[] // default: []
```
Dependency array for the callback, just like React's `useCallback` or `useMemo`. If your callback references unstable or changing values, add them here so the callback stays up to date.
See the [Callback Dependencies](/docs/documentation/useHotkeys/setting-callback-dependencies) documentation for examples.
### Return value
#### `React.RefCallback`
`useHotkeys` returns a React ref callback. Attach it to any element to scope the hotkey so it only fires when that element (or one of its children) has focus.
```jsx live
function App() {
const [count, setCount] = useState(0);
const ref = useHotkeys("s", () => setCount((prevCount) => prevCount + 1));
return (
Count: {count}
);
}
```
:::info Non-focusable elements need tabIndex
Elements without native interactivity — like `
`, ``, and `
` — cannot receive focus by default. Add a `tabIndex` prop to make them focusable:
```jsx live
function App() {
const [count, setCount] = useState(0);
const ref = useHotkeys("s", () => setCount((prevCount) => prevCount + 1));
return (
Count: {count}
Focusing this area won't trigger the hotkey.
Focusing this area will trigger the hotkey.
);
}
```
:::
### Function signature overloads
If you want to pass a dependency array but do not need any options, you can pass the array as the third argument instead of the fourth:
```ts
function useHotkeys(
keys: string,
callback: (event: KeyboardEvent, handler: HotkeysEvent) => void,
deps: any[] = []
): React.RefCallback
```
So instead of:
```js
useHotkeys('a', () => someDependency, undefined, [someDependency]);
```
You can write:
```js
useHotkeys('a', () => someDependency, [someDependency]);
```
---
## useRecordHotkeys(Api)
## useRecordHotkeys API
Function signature:
```ts
function useRecordHotkeys(
useKey?: boolean,
blacklist?: string[]
): [Set, {
start: () => void,
stop: () => void,
resetKeys: () => void,
isRecording: boolean
}]
```
### `useKey`
```ts
useKey: boolean // default: false
```
By default, the hook records key codes (physical keys). Pass `true` to record the produced character instead.
* `useKey: false` — records `"shift+1"` when the user presses Shift+1
* `useKey: true` — records `"!"` when the user presses Shift+1 on a US layout
### `blacklist`
```ts
blacklist: string[] // default: []
```
Array of key names to ignore while recording. Blacklisted keys retain their default behavior and are not added to the recorded set.
```js
const [keys, { start, stop }] = useRecordHotkeys(false, ['tab', 'enter']);
```
***
### Return value
```ts
const [keys, { start, stop, resetKeys, isRecording }] = useRecordHotkeys()
```
#### `keys`
```ts
keys: Set
```
A `Set` of the keys that have been recorded. Each key appears only once. Use `Array.from(keys)` to convert it to an array.
#### `start`
```ts
start: () => void
```
Begins recording keystrokes.
#### `stop`
```ts
stop: () => void
```
Stops recording keystrokes.
#### `resetKeys`
```ts
resetKeys: () => void
```
Clears the recorded keys.
#### `isRecording`
```ts
isRecording: boolean
```
`true` while recording is active, `false` otherwise.
---
## Migrate from 4.x to 5.x
Version 5 introduces a few breaking changes. This guide walks you through updating your code.
### Behavior changes
#### `useHotkeys` listens to physical keys by default
Starting with version 5, `useHotkeys` listens to the physical key **code** instead of the produced **character** by default.
**What this means:**
* In v4, `useHotkeys('!', callback)` would trigger when the user typed `!`.
* In v5, `useHotkeys('!', callback)` does **not** work by default, because `!` is not a physical key code.
**How to migrate:**
If you were listening to special characters, you have two options:
1. **Listen to the physical keystroke** (layout-independent):
```js
// v5 default behavior
useHotkeys('shift+1', callback)
```
2. **Listen to the produced character** (layout-dependent) — use the `useKey` option:
```js
useHotkeys('!', callback, { useKey: true })
```
See the [Keyboard Layouts](/docs/documentation/useHotkeys/ignore-layouts) documentation for more details.
#### `HotkeysProvider` scope behavior
If **all** scopes are disabled in `HotkeysProvider`, no hotkeys will be active.
### API changes
#### `useHotkeys` options renamed
In v4, the option names for combining and separating keys were confusing:
| v4 name | v5 name | Purpose |
|---------|---------|---------|
| `combinationKey` | `splitKey` | Character that joins keys within a combination (default: `+`) |
| `splitKey` | `delimiter` | Character that separates different hotkey combinations (default: `,`) |
**Migration:**
```js
// v4
useHotkeys('ctrl+a, shift+b', callback, { combinationKey: '-', splitKey: ';' })
// v5
useHotkeys('ctrl+a, shift+b', callback, { splitKey: '-', delimiter: ';' })
```
#### `HotkeysProvider` context
##### `enabledScopes` renamed to `activeScopes`
To avoid confusion with the `enableScope` method, `enabledScopes` has been renamed to `activeScopes`.
```js
// v4
const { enabledScopes } = useHotkeysContext()
// v5
const { activeScopes } = useHotkeysContext()
```
### Other changes
- **Dropped CommonJS support.** The package is now ESM-only. If you need CommonJS, use a bundler like Webpack, Rollup, or Vite to handle the conversion.