The useHotkey hook is the primary way to register keyboard shortcuts in React applications. It wraps the singleton HotkeyManager with automatic lifecycle management, stale-closure prevention, and React ref support.
import { useHotkey } from '@tanstack/react-hotkeys'
function App() {
useHotkey('Mod+S', () => {
saveDocument()
}, {
// override the default options here
})
}
The callback receives the original KeyboardEvent as the first argument and a HotkeyCallbackContext as the second:
useHotkey('Mod+S', (event, context) => {
console.log(context.hotkey) // 'Mod+S'
console.log(context.parsedHotkey) // { key: 'S', ctrl: false, shift: false, alt: false, meta: true, modifiers: ['Meta'] }
})
You can pass a hotkey as a string or as a RawHotkey object (modifier booleans optional). Use mod for cross-platform shortcuts (Command on Mac, Control elsewhere):
useHotkey('Mod+S', () => save())
useHotkey({ key: 'S', mod: true }, () => save()) // Same as above
useHotkey({ key: 'Escape' }, () => closeModal())
useHotkey({ key: 'S', ctrl: true, shift: true }, () => saveAs())
useHotkey({ key: 'S', mod: true, shift: true }, () => saveAs())
When you register a hotkey without passing options, or when you omit specific options, the following defaults apply:
useHotkey('Mod+S', callback, {
enabled: true,
preventDefault: true,
stopPropagation: true,
eventType: 'keydown',
requireReset: false,
ignoreInputs: true,
target: document,
platform: undefined, // auto-detected
conflictBehavior: 'warn',
})
Why these defaults? Most hotkey registrations aim to override browser behavior (e.g., Mod+S for save instead of the browser's "Save Page" dialog). The library defaults preventDefault and stopPropagation to true so your hotkeys take precedence without extra boilerplate. Input-like elements (inputs, textareas, contenteditable) are ignored by default (ignoreInputs: true) so shortcuts don't fire while the user is typing. When a hotkey is already registered, a warning is logged (conflictBehavior: 'warn') to help catch duplicate bindings during development.
You can change the default options for all useHotkey calls in your app by wrapping your component tree with HotkeysProvider. Per-hook options will override the provider defaults.
import { HotkeysProvider } from '@tanstack/react-hotkeys'
<HotkeysProvider
defaultOptions={{
hotkey: { preventDefault: false, ignoreInputs: false },
}}
>
<App />
</HotkeysProvider>
Controls whether the hotkey is active. Defaults to true.
const [isEditing, setIsEditing] = useState(false)
// Only active when editing
useHotkey('Mod+S', () => save(), { enabled: isEditing })
Automatically calls event.preventDefault() when the hotkey fires. Defaults to true.
// Browser default is prevented by default
useHotkey('Mod+S', () => save())
// Opt out when you want the browser's default behavior
useHotkey('Mod+S', () => save(), { preventDefault: false })
Calls event.stopPropagation() when the hotkey fires. Defaults to true.
// Event propagation is stopped by default
useHotkey('Escape', () => closeModal())
// Opt out when you need the event to bubble
useHotkey('Escape', () => closeModal(), { stopPropagation: false })
Whether to listen on keydown (default) or keyup.
// Fire when the key is released
useHotkey('Shift', () => deactivateMode(), { eventType: 'keyup' })
When true, the hotkey will only fire once per key press. The key must be released and pressed again to fire again. Defaults to false.
// Only fires once per Escape press, not on key repeat
useHotkey('Escape', () => closePanel(), { requireReset: true })
When true (the default), the hotkey will not fire when the user is focused on an input, textarea, select, or contentEditable element. This prevents hotkeys from interfering with text input.
// This will NOT fire when typing in an input
useHotkey('K', () => openSearch())
// This WILL fire even when typing in an input
useHotkey('Escape', () => closeDialog(), { ignoreInputs: false })
Set ignoreInputs: false for hotkeys that should always work, like Escape to close a modal.
The DOM element to attach the event listener to. Defaults to document. Can be a DOM element, document, window, or a React ref.
import { useRef } from 'react'
function Panel() {
const panelRef = useRef<HTMLDivElement>(null)
// Only listens for events on this specific element
useHotkey('Escape', () => closePanel(), { target: panelRef })
return (
<div ref={panelRef} tabIndex={0}>
<p>Panel content</p>
</div>
)
}
When using a ref as the target, make sure the element is focusable (has tabIndex) so it can receive keyboard events.
Controls what happens when you register a hotkey that's already registered. Options:
useHotkey('Mod+S', () => save(), { conflictBehavior: 'replace' })
Override the auto-detected platform. Useful for testing or for applications that need to force a specific platform behavior.
useHotkey('Mod+S', () => save(), { platform: 'mac' })
The useHotkey hook automatically syncs the callback on every render, so you never need to worry about stale closures:
function Counter() {
const [count, setCount] = useState(0)
// This callback always has access to the latest `count` value
useHotkey('Mod+Shift+C', () => {
console.log('Current count:', count)
})
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}
The hook automatically unregisters the hotkey when the component unmounts:
function TemporaryPanel() {
// Automatically cleaned up when this component unmounts
useHotkey('Escape', () => closePanel())
return <div>Panel content</div>
}
Under the hood, useHotkey uses the singleton HotkeyManager. You can also access the manager directly if needed:
import { getHotkeyManager } from '@tanstack/react-hotkeys'
const manager = getHotkeyManager()
// Check if a hotkey is registered
manager.isRegistered('Mod+S')
// Get total number of registrations
manager.getRegistrationCount()
The manager attaches event listeners per target element, so only elements that have registered hotkeys receive listeners. This is more efficient than a single global listener.