Skip to main content
Version: 5.0

useHotkeys API

Function signature:

function useHotkeys<T extends Element>(
keys: string | string[],
callback: (event: KeyboardEvent, handler: HotkeysEvent) => void,
options: Options = {},
deps: any[] = []
): React.RefCallback<T | null>

Arguments​

keys​

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​

useHotkeys('*', (_, handler) => alert(handler.key))

Use modifiers​

useHotkeys('ctrl+s, shift+w', () => alert('We're using modifiers now!'))

Use function keys​

useHotkeys('f5', () => alert('F5 was pressed'))

Multiple key combinations​

useHotkeys('w, a, s, d', () => alert('Player moved!'))
Differentiate between multiple keys

When multiple possible keys share the same callback, use handler.keys to determine which one was pressed:

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:

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:

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​

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 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<string, unknown> — Custom metadata assigned via options.
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.


options​

Configure the hook's behavior by passing an options object.

// 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,
};
// 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<string, unknown>
};

enabled​

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​

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:

useHotkeys('ctrl+s', save, { enableOnFormTags: ['input', 'textarea', 'select'] })

enableOnContentEditable​

enableOnContentEditable: boolean // default: false

Enable hotkeys on elements with the contentEditable attribute.

ignoreEventWhen​

ignoreEventWhen: (e: KeyboardEvent) => boolean // default: undefined

Fine-grained control over which events to ignore. Return true from the function to skip handling the event:

useHotkeys('a', someCallback, {
ignoreEventWhen: (e) => {
return e.target.className.includes('special-element')
},
})

splitKey​

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:

useHotkeys('ctrl-+', zoomIn, { splitKey: '-' })

delimiter​

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​

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​

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 documentation for details.

keyup​

keyup: boolean // default: false

Trigger the callback on the browser's keyup event.

keydown​

keydown: boolean // default: true

Trigger the callback on the browser's keydown event. This is the default.

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:

useHotkeys('a', callback, {
keydown: true,
keyup: true
})

preventDefault​

preventDefault: Trigger // default: false

Prevent the browser's default behavior for the matched keystroke. Useful for overriding shortcuts like meta+s (save page).

useHotkeys('meta+s', someCallback, {
preventDefault: true,
});

description​

description: string // default: undefined

A human-readable description of what the hotkey does. Useful for building help panels or shortcut reference lists.

document​

document: Document // default: undefined

Bind the event listeners to a specific Document object instead of the global document. Useful for apps running inside iframes.

import FrameComponent from 'react-frame-component'

const InsideFrameComponent = () => {
const { document } = useFrame()

useHotkeys("s", () => console.log("Triggered inside iframe"), { document })

return <div>....</div>
}

ignoreModifiers​

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.

function App() {
useHotkeys('/', () => inputRef.current?.focus(), { ignoreModifiers: true, preventDefault: true })

const inputRef = useRef<HTMLInputElement>(null)

return (
<input type='text' ref={inputRef} placeholder="search via '/'" />
)
}
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​

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​

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​

eventListenerOptions: EventListenerOptions // default: undefined

Pass custom options to the underlying addEventListener call, such as { passive: true } or { capture: true }.

metadata​

metadata: Record<string, unknown> // 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​

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 documentation for examples.

Return value​

React.RefCallback<T | null>​

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.

Live Editor
function App() {
  const [count, setCount] = useState(0);
  const ref = useHotkeys("s", () => setCount((prevCount) => prevCount + 1));

  return (
    <div>
      <div style={{ padding: "30px" }}>Count: {count}</div>
      <button style={{ padding: "30px", background: "teal" }}>
        Focusing this area won't trigger the hotkey.
      </button>
      <button style={{ padding: "30px", background: "crimson" }} ref={ref}>
        Focusing this area will trigger the hotkey.
      </button>
    </div>
  );
}
Result
Loading...
Non-focusable elements need tabIndex

Elements without native interactivity — like <div>, <span>, and <p> — cannot receive focus by default. Add a tabIndex prop to make them focusable:

Live Editor
function App() {
  const [count, setCount] = useState(0);
  const ref = useHotkeys("s", () => setCount((prevCount) => prevCount + 1));

  return (
    <div>
      <div style={{ padding: "30px" }}>Count: {count}</div>
      <div style={{ padding: "30px", background: "teal" }} tabIndex={0}>
        Focusing this area won't trigger the hotkey.
      </div>
      <div style={{ padding: "30px", background: "crimson" }} ref={ref} tabIndex={0}>
        Focusing this area will trigger the hotkey.
      </div>
    </div>
  );
}
Result
Loading...

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:

function useHotkeys<T extends Element>(
keys: string,
callback: (event: KeyboardEvent, handler: HotkeysEvent) => void,
deps: any[] = []
): React.RefCallback<T | null>

So instead of:

useHotkeys('a', () => someDependency, undefined, [someDependency]);

You can write:

useHotkeys('a', () => someDependency, [someDependency]);