Utilities
Hotkeys

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

PropDefaultType
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
HotkeyOptions

Options applied to every command registered under this provider. Per-command options override it.

sequenceTimeoutMs1000
number

How long a sequence like G > H waits for the next key before resetting.

useHotkey

PropDefaultType
hotkey
string

The 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) => void

Called when the hotkey fires.

options
UseHotkeyOptions

Everything on UseHotkeysCommand except id, hotkey and action. The id is generated for you.

useHotkeys

PropDefaultType
commands
UseHotkeysCommand[]

The commands to register. Each is keyed by id, so changing one re-registers only that command.

UseHotkeysCommand

PropDefaultType
id
string

Unique identifier. Used to reconcile registrations across renders and to unregister on unmount.

hotkey
string

The key combination or sequence that triggers the command.

action
(event: KeyboardEvent) => void

Called when the hotkey fires.

label
string

Human-readable name. Read back by useHotkeyRegistrations, so a command palette can render it.

description
string

Longer explanation of what the command does.

category
string

Group 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.

enabledtrue
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
HotkeyOptions

Per-command behavior. Overrides the provider defaultOptions.

HotkeyOptions

PropDefaultType
preventDefault
boolean

Call preventDefault() on the event before running the action.

stopPropagation
boolean

Call stopPropagation() on the event before running the action.

enableOnFormTagsfalse
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.

enableOnContentEditablefalse
boolean

Whether the shortcut fires inside a contenteditable element.

capturetrue
boolean

Listen in the capture phase.

requireResetfalse
boolean

Fire 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

PropDefaultType
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

PropDefaultType
returns
HotkeyStore

The 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

PropDefaultType
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

PropDefaultType
hotkey
string

The key or combination to watch.

returns
boolean

Whether that key or combination is currently held.

usePlatform

PropDefaultType
returns
Platform

The current platform, one of 'mac', 'windows' or 'linux'. Resolves after mount, so server and client render the same markup.

useFormatHotkey

PropDefaultType
returns
(hotkey: string, options?: HotkeyFormatOptions) => string

A 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

PropDefaultType
props
UseHotkeyRecorderProps

Accepts onRecord, onCancel, onClear, formatOptions and sequenceTimeoutMs.

returns
UseHotkeyRecorderReturn

The recorder handle, described below.

UseHotkeyRecorderReturn

PropDefaultType
recording
boolean

Whether the recorder is currently listening for key events.

value
RecordedHotkey | null

The hotkey recorded so far. value is the raw string, display is the platform-formatted one.

start
() => void

Start listening for key events.

stop
() => void

Stop listening and keep the recorded hotkey.

cancel
() => void

Stop listening and discard the recorded hotkey. Also triggered by Escape.

clear
() => void

Clear the recorded hotkey. Also triggered by Backspace or Delete.