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 an id,
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.
Sharing one store
Without a provider, every hook shares a default store. Wrap your tree in HotkeysProvider to set defaults for every
command, control active scopes, or isolate a subtree.
<HotkeysProvider activeScopes={['editor']} conflictBehavior="replace" sequenceTimeoutMs={800}>
<App />
</HotkeysProvider>
defaultOptions applies to every command registered under it, and per-command options override it.
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('mod+S', 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
HotkeysProvider
| Prop | Default | Type |
|---|---|---|
activeScopes | ['*'] | string | string[]The scopes that are 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 under this provider. Per-command options override it. | |
sequenceTimeoutMs | 1000 | numberHow long a sequence like G > H waits for the next key before resetting. |
useHotkey
| Prop | Default | Type |
|---|---|---|
hotkey | stringThe key combination, such as mod+K, shift+alt+D or the sequence G > H. mod resolves to Command on macOS and Control elsewhere. | |
action | (event: KeyboardEvent) => voidCalled when the hotkey fires. | |
options | UseHotkeyOptionsEverything on UseHotkeysCommand except id, hotkey and action. The id is generated for you. |
useHotkeys
| Prop | Default | Type |
|---|---|---|
commands | UseHotkeysCommand[]The commands to register. Each is keyed by id, so changing one re-registers only that command. |
UseHotkeysCommand
| Prop | Default | Type |
|---|---|---|
id | stringUnique identifier. Used to reconcile registrations across renders and to unregister on unmount. | |
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 |
|---|---|---|
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 from the nearest HotkeysProvider, or a shared default store if there is none. Use setScope, addScope, removeScope and toggleScope to change which commands are live, and isPressed to test a combination directly. |
usePressedKeys
| Prop | Default | Type |
|---|---|---|
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. | |
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. |