powerkeys is a keyboard runtime for web apps that need more than a few flat
keydown listeners. It gives you declarative bindings, layered scopes,
multi-step sequences, when clauses, shortcut recording, atomic rebinding
through binding sets, and a shared availability check for external actions.
The important boundary is this:
powerkeysowns keyboard matching, dispatch, recording, and evaluation ofscopepluswhen.- Your app owns commands, command-palette items, UI state, persistence, and any metadata such as title, group, or search keywords.
If you already have a command model, powerkeys should plug into it. The usual
shape is to keep the action in your app, reuse its scope and when when you
attach a shortcut, and call isAvailable when a palette or menu needs to know
whether that action currently makes sense.
Use powerkeys when your app needs one or more of these:
- layered shortcut scopes such as modal over editor over root
- shortcuts that depend on app state such as selection, mode, or read-only
- multi-step sequences such as
g gorg i - user-recordable shortcut expressions
- shortcuts derived from persisted user preferences or other mutable app state that need atomic replacement
- one source of truth for shortcut eligibility and external action availability
- a DOM boundary narrower than the whole document
powerkeys is a poor fit when you need one or more of these instead:
- OS-level or browser-global shortcuts outside the current document
- a full command system or palette framework that already owns keyboard input
- direct DOM listeners with no meaningful state, scope, or conflict rules
- a library that should own your command registry, menu model, or UI rendering
ShortcutRuntime
- Create one with
createShortcuts({ target, ... }). - It owns event listeners, binding registration, runtime context, recording, and availability checks.
Bindings
- A binding is either one combo such as
Mod+kor a sequence such asg g. - A binding always has a handler.
- A binding may also declare
scope,when,priority,editablePolicy,keyEvent, and event-consumption behavior.
Scopes
- Active scopes come from
getActiveScopes. - Earlier scopes have higher precedence.
rootis always appended, so unscoped bindings and actions remain eligible.- Scopes are the coarse filter for "which layer of the app is active right now?"
when Clauses
- A
whenclause is a boolean expression evaluated against runtime context. - Use it for finer-grained state such as
editor.hasSelection && !editor.readOnly. whenclauses can be shared between your own action objects and shortcut bindings.
Runtime Context
- Write context with
setContextorbatchContext. - Built-in namespaces are
event,scope,runtime, andcontext. - User context is also spread onto the top level, so
editor.hasSelectionis readable directly in awhenclause.
Availability Checks
isAvailable({ scope, when })answers whether an external action is currently eligible.- The input is structural. Extra fields on your own action objects are ignored.
isAvailableevaluates only shared availability concerns. It does not know about command ids, palette sections, search text, or rendering.- For external checks, the
eventnamespace is inert:event.keyandevent.codeareundefined, and modifier booleans arefalse.
Recording
record()captures canonical shortcut expressions from live input.- Recording is separate from registration. The common flow is: record, persist
the expression, then later bind it directly or swap it into a
BindingSet.
Binding Sets
- Create one with
shortcuts.createBindingSet(). - A binding set owns a mutable collection of bindings that can be replaced as one unit.
replace(nextBindings)validates the whole next collection before swapping it into place.- Failed replacement leaves the current bindings unchanged.
- Successful replacement drops any in-progress sequence state owned by the previous set contents.
- Create a runtime with a document or element
target. - Keep your app's real state in your app, and mirror only the parts relevant to shortcut eligibility into runtime context.
- Return active scopes from
getActiveScopesin precedence order. - Register bindings with
bindfor static shortcuts orshortcuts.createBindingSet()for derived shortcut collections that need atomic replacement. - If your app has its own command or action objects, put shared availability on
those objects with
scopeandwhen. - Reuse that same
scopeandwhenwhen attaching a keyboard shortcut. - Call
isAvailablewhen an external surface such as a command palette needs to know whether an action should be offered right now. - Dispose the runtime when the owning UI subtree or application shuts down.
Open a command palette
bind({ combo: "Mod+k", preventDefault: true, handler })
Keep modal shortcuts above editor shortcuts
getActiveScopes: () => ["modal", "editor"]- Bind modal and editor actions to the same combo with different scopes
Gate a shortcut on app state
setContext("editor.hasSelection", true)bind({ combo: "c", when: "editor.hasSelection", handler })
Share availability rules with an external command palette
- Put
scopeandwhenon your own action object isAvailable(action)before rendering or invoking it from the palette- Reuse that same
scopeandwheninbind({ ..., handler })
Register multi-step navigation
bind({ sequence: "g g", handler })- Adjust
sequenceTimeoutwhen the default one-second window is not right for your app
Temporarily disable keyboard shortcuts
pause(scope)andresume(scope)- Omit the scope to pause or resume the whole runtime
pauseaffects keyboard dispatch only. It does not make external actions unavailable toisAvailable
Let users choose their own shortcut
record({ onUpdate, suppressHandlers: true })- Save the returned
ShortcutRecording.expression - Rebind that expression later with
bindorBindingSet.replace
Rebind a user-configurable shortcut set
const userBindings = shortcuts.createBindingSet()- Recompute your next object-form bindings in app code
userBindings.replace(nextBindings)to swap them atomically
Debug why a shortcut did not fire
explain(event)to inspect scope, matcher, andwhen-clause decisions
- Keep your command or action model in your app, and treat
powerkeysas the keyboard and availability layer. - Use one long-lived
BindingSetwhen shortcuts are derived from mutable app state or persisted user preferences and must be replaced as one unit. - Use scopes for major UI layers such as modal, editor, sidebar, and root.
- Use
whenfor state that changes frequently inside one scope, such as selection state or read-only mode. - Reuse one
scopepluswhenrule across all invocation surfaces for the same action. - Mirror only decision-making state into runtime context. If a value does not affect eligibility, it probably does not belong there.
- Do not build palette presentation concerns such as group names, labels, or
search keywords into
powerkeys. - Do not use
pauseas a visibility switch for menus or palettes. It is a keyboard-only control. - Do not rebuild one dynamic shortcut collection with manual unbind and rebind
loops when one
BindingSetcan own that collection. - Do not make shared availability depend on keyboard-event details such as
event.keyor modifier state. - Do not duplicate the same eligibility rule in separate shortcut-only and
palette-only code paths when one shared
scopepluswhenclause will do. - Do not treat
getActiveScopesas a place for fine-grained state that belongs inwhen.
rootis always active, even whengetActiveScopesreturns nothing.- Each binding must define exactly one of
comboorsequence. - Only one recording session may be active per runtime.
- Only one binding wins a given event.
BindingSet.replaceis atomic: invalid next bindings do not partially update the active set.- Editable targets are blocked by default.
- Reserved top-level context names are
context,event,scope, andruntime. - Sequence state expires after
sequenceTimeoutmilliseconds of inactivity. pauseandresumeare reference-counted, so repeated pauses require matching resumes.
- Invalid binding definitions throw synchronously during
bindorBindingSet.replace. - Failed
BindingSet.replacecalls leave the set unchanged. - Invalid
whensyntax also throws synchronously duringisAvailable. - Handler errors are sent to
onErrorwhen provided; otherwise they are rethrown asynchronously. when-clause evaluation errors during dispatch do not throw through the native event handler. They cause that binding to fail itswhencheck, and the error appears inexplain.when-clause evaluation errors insideisAvailablereturnfalse.- Recording
onUpdateerrors are reported throughonErrorand do not cancel the active recording. - Cancelling a recording rejects
RecordingSession.finishedwith anAbortError.
-
Combo
- One key press plus zero or more modifiers, such as
Ctrl+korMeta+/.
- One key press plus zero or more modifiers, such as
-
Sequence
- Whitespace-separated combo steps, such as
g g.
- Whitespace-separated combo steps, such as
-
Scope
- A named dispatch layer used to decide which bindings or external actions are
eligible before
whenclauses run.
- A named dispatch layer used to decide which bindings or external actions are
eligible before
-
When Clause
- A boolean expression evaluated against runtime context to make a final eligibility decision.
-
Boundary
- The document or element passed as
target, which limits which native events the runtime considers.
- The document or element passed as
-
Editable Policy
- The rule that decides whether a binding may run while focus is inside an editable element.
-
Binding Set
- A runtime-owned collection of bindings that can be replaced, cleared, or disposed as one unit.
- global shortcuts outside the current DOM boundary
- command registration or command identifiers owned by
powerkeys - command-palette or menu rendering
- framework-specific hooks or adapters