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

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

Options applied to every command registered on this store. Per-command options override it.

sequenceTimeoutMs1000
number

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

returns
HotkeyStore

The 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

PropDefaultType
props
UseHotkeyProps

One command, plus an optional store. Takes every field of UseHotkeysCommand.

useHotkeys

PropDefaultType
commands
UseHotkeysCommand[]

The commands to register. Passed inside a single object, so the whole argument is { commands, store, id }.

store
HotkeyStore

The store to register on. Defaults to a store shared by every hook that does not name one.

id
string

Prefix for the ids generated for commands that do not set their own. One is generated when omitted.

UseHotkeysCommand

PropDefaultType
id
string

Identifies 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
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
store
HotkeyStore

The 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

PropDefaultType
returns
HotkeyStore

The 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

PropDefaultType
store
HotkeyStore

The 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

PropDefaultType
hotkey
string

The key or combination to watch. Passed inside a single object, so the whole argument is { hotkey, store }.

store
HotkeyStore

The store to read from. Defaults to a store shared by every hook that does not name one.

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.