# 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 (

The count is {count}.

) } function SecondUnscopedHotkey() { const [count, setCount] = useState(0) useHotkeys('c', () => setCount(prevCount => prevCount + 1)) return (

The count is {count}.

) } render(
) ``` 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.

) } function SecondScopedHotkey() { 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.

) } 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.

) } function ScopedHotkey({children}) { 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.

{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.