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]);