Hotkeys
Register keyboard shortcuts, sequences and scopes, and read them back to build a command palette.
Setup
Register a shortcut with useHotkey. It listens on the document and cleans up on unmount.
mod resolves to Command on macOS and Control elsewhere, so you write the binding once.
Examples
Multiple shortcuts
useHotkeys takes an array, which is what you want when the list comes from data. Each command needs a hotkey and an
action.
Sequences
Press one key, then another. G > H fires only if both land inside the sequence window.
Sequence timeout
sequenceTimeoutMs sets that window, which defaults to one second. Wait longer and the sequence resets without firing.
Scopes
A command with scopes: ['editor'] only fires while that scope is active. store.setScope swaps an entire set of
shortcuts at once.
Form fields
Single keys are ignored while you type in an input, textarea or select. Shortcuts with a modifier still fire, because
Cmd+S in a text field still means save. Opt a single key back in with options: { enableOnFormTags: true }.
Conflicts
When two commands claim the same shortcut, conflictBehavior decides. warn is the default and keeps both, replace
drops the earlier one, allow keeps both silently, error refuses the second.
Key state
usePressedKeys returns the keys currently held, useIsKeyPressed answers for one. Use these when a held key changes
what an interaction means, like Shift to extend a selection.
Recording a shortcut
useHotkeyRecorder captures whatever the user presses, for a "click to rebind" setting. Escape cancels, Backspace
clears, and chords and sequences both work.
Command palette
useHotkeyRegistrations returns every registered command with its metadata, so the palette is a view over the registry
instead of a second list you keep in sync.
Register once with label, category and keywords, then group by category, search keywords, and call
item.action on select. Here, typing "dark" finds "Toggle theme" even though its label has no "dark" in it.
Guides
Displaying a shortcut
useFormatHotkey returns a formatter bound to the current platform, so mod+K renders as ⌘ K on macOS and Ctrl K
elsewhere.
const formatHotkey = useFormatHotkey()
return <kbd>{formatHotkey('mod+K')}</kbd>
Use formatHotkey from @zag-js/hotkeys only outside a component. Inside one it reads the platform during render,
which mismatches on hydration when the server says Ctrl K and the browser says ⌘ K. For the platform itself,
usePlatform returns mac, windows or linux.
Using your own store
Without one, every hook registers on a shared default store. Create your own with createHotkeyStore and pass it to any
hook to isolate a set of commands, set defaults for all of them, or control active scopes.
const store = createHotkeyStore({
activeScopes: ['editor'],
conflictBehavior: 'replace',
sequenceTimeoutMs: 800,
})
useHotkeys({ commands, store })
useHotkey({ hotkey: 'mod+S', action: save, store })
Build the store outside the component, or memoize it. One created during render is rebuilt on every render and loses its registrations.
A command palette is the usual reason to reach for this: give it its own store and useHotkeyRegistrations({ store })
returns only the commands registered on it, instead of everything on the page.
Enabling and disabling
Pass enabled as a boolean or a function. A function is re-evaluated each time the key fires, so it reads current state
without re-registering.
useHotkey({ hotkey: 'mod+S', action: save, enabled: () => !isReadOnly })
Reacting to a key release
options: { eventType: 'keyup' } fires on release instead of press. Pair it with a keydown command on the same key
for push-to-talk.
API Reference
createHotkeyStore
| Prop | Default | Type |
|---|---|---|
activeScopes | ['*'] | string | string[]The scopes that start active. Only commands in an active scope fire. |
conflictBehavior | 'warn' | 'warn' | 'error' | 'replace' | 'allow'What to do when two commands register the same hotkey. warn keeps both and logs, replace drops the earlier one, allow keeps both silently, error refuses the second. |
defaultOptions | HotkeyOptionsOptions applied to every command registered on this store. Per-command options override it. | |
sequenceTimeoutMs | 1000 | numberHow long a sequence like G > H waits for the next key before resetting. |
returns | HotkeyStoreThe store. Pass it to any hook as store, and keep it stable: build it outside the component or memoize it, since one created during render is rebuilt on every render and loses its registrations. |
useHotkey
| Prop | Default | Type |
|---|---|---|
props | UseHotkeyPropsOne command, plus an optional store. Takes every field of UseHotkeysCommand. |
useHotkeys
| Prop | Default | Type |
|---|---|---|
commands | UseHotkeysCommand[]The commands to register. Passed inside a single object, so the whole argument is { commands, store, id }. | |
store | HotkeyStoreThe store to register on. Defaults to a store shared by every hook that does not name one. | |
id | stringPrefix for the ids generated for commands that do not set their own. One is generated when omitted. |
UseHotkeysCommand
| Prop | Default | Type |
|---|---|---|
id | stringIdentifies the command across renders. One is generated when omitted, keyed by position within the hook instance, which is enough unless something else needs to address the command by name. | |
hotkey | stringThe key combination or sequence that triggers the command. | |
action | (event: KeyboardEvent) => voidCalled when the hotkey fires. | |
label | stringHuman-readable name. Read back by useHotkeyRegistrations, so a command palette can render it. | |
description | stringLonger explanation of what the command does. | |
category | stringGroup name, for sectioning a command palette. | |
keywords | string[]Alternative search terms. Lets a palette match "dark" against a command labelled "Toggle theme". | |
scopes | '*' | string | string[]The scopes this command belongs to. It only fires while one of them is active. |
enabled | true | boolean | (() => boolean)Whether the command can fire. A function is re-evaluated on every key press, so it reads current state without re-registering. |
options | HotkeyOptionsPer-command behavior. Overrides the provider defaultOptions. |
HotkeyOptions
| Prop | Default | Type |
|---|---|---|
preventDefault | booleanCall preventDefault() on the event before running the action. | |
stopPropagation | booleanCall stopPropagation() on the event before running the action. | |
enableOnFormTags | false | boolean | ('input' | 'textarea' | 'select')[]Whether a single-key shortcut fires while an input, textarea or select has focus. Shortcuts with a modifier always fire. Pass an array to opt in to specific tags. |
enableOnContentEditable | false | booleanWhether the shortcut fires inside a contenteditable element. |
capture | true | booleanListen in the capture phase. |
requireReset | false | booleanFire once per press. The key must be released before it fires again, which suppresses key repeat. |
eventType | 'keydown' | 'keydown' | 'keyup'Whether to fire on press or on release. Pair a keyup command with a keydown one on the same key for push-to-talk. |
target | Element | (() => Element | null)Scope the command to a DOM subtree. It only fires when the event originates inside this element, which must contain focus. Resolved on every event, and skipped while it resolves to null. |
useHotkeyRegistrations
| Prop | Default | Type |
|---|---|---|
store | HotkeyStoreThe store to read from. Defaults to a store shared by every hook that does not name one. | |
returns | HotkeyCommand[]Every command currently registered on the store, with its label, category, keywords and resolved hotkey. Re-reads when commands are added or removed, which is what lets a command palette be a view over the registry instead of a second list to keep in sync. |
useHotkeyStore
| Prop | Default | Type |
|---|---|---|
returns | HotkeyStoreThe store you passed, or the shared default store. Use setScope, addScope, removeScope and toggleScope to change which commands are live, and isPressed to test a combination directly. |
usePressedKeys
| Prop | Default | Type |
|---|---|---|
store | HotkeyStoreThe store to read from. Defaults to a store shared by every hook that does not name one. | |
returns | string[]The keys currently held down. Use it when a held key changes what an interaction means, such as Shift to extend a selection. |
useIsKeyPressed
| Prop | Default | Type |
|---|---|---|
hotkey | stringThe key or combination to watch. Passed inside a single object, so the whole argument is { hotkey, store }. | |
store | HotkeyStoreThe store to read from. Defaults to a store shared by every hook that does not name one. | |
returns | booleanWhether that key or combination is currently held. |
usePlatform
| Prop | Default | Type |
|---|---|---|
returns | PlatformThe current platform, one of 'mac', 'windows' or 'linux'. Resolves after mount, so server and client render the same markup. |
useFormatHotkey
| Prop | Default | Type |
|---|---|---|
returns | (hotkey: string, options?: HotkeyFormatOptions) => stringA formatter bound to the current platform, so mod+K renders as ⌘ K on macOS and Ctrl K elsewhere. Prefer this over importing formatHotkey directly inside a component, which reads the platform during render and mismatches on hydration. |
useHotkeyRecorder
| Prop | Default | Type |
|---|---|---|
props | UseHotkeyRecorderPropsAccepts onRecord, onCancel, onClear, formatOptions and sequenceTimeoutMs. | |
returns | UseHotkeyRecorderReturnThe recorder handle, described below. |
UseHotkeyRecorderReturn
| Prop | Default | Type |
|---|---|---|
recording | booleanWhether the recorder is currently listening for key events. | |
value | RecordedHotkey | nullThe hotkey recorded so far. value is the raw string, display is the platform-formatted one. | |
start | () => voidStart listening for key events. | |
stop | () => voidStop listening and keep the recorded hotkey. | |
cancel | () => voidStop listening and discard the recorded hotkey. Also triggered by Escape. | |
clear | () => voidClear the recorded hotkey. Also triggered by Backspace or Delete. |