hotkey-router
v0.0.3
Published
A tiny, deterministic keyboard routing engine for modern web apps.
Maintainers
Readme
hotkey-router
A tiny, deterministic keyboard routing engine for modern web apps.
Hotkey Router is not a key utility. It is a predictable, plugin-first routing layer for keyboard shortcuts.
- ⚡ O(1) dispatch
- 🧩 Plugin-safe lifecycle management
- 🎯 Deterministic winner selection (priority + recency)
- 🛑 Input-safe by default
- 🧪 Testable via
trigger() - 📦 4.9 kB minified
- 🗜 2.2 kB minified + gzipped
- 🚫 Zero dependencies
Philosophy
Hotkey Router follows three core rules:
- Predictable routing — Highest priority wins. Ties go to the most recently bound handler.
- Safe composition — Plugins can register and unregister without affecting others.
- Modern only — Built for modern browsers using
KeyboardEvent.key.
No keycodes. No legacy IE hacks. No hidden global scope state.
Install
npm install hotkey-routerESM
import hotkeys from 'hotkey-router'CommonJS
const hotkeys = require('hotkey-router')CDN (ESM)
import hotkeys from 'https://cdn.jsdelivr.net/npm/hotkey-router/dist/hotkey-router.min.js'Basic Usage
import hotkeys from 'hotkey-router'
// Simple keydown
hotkeys.bind('ctrl+k', () => {
openCommandPalette()
})
// Keyup using " up" suffix
hotkeys.bind('ctrl+p up', () => {
console.log('Released CTRL+P')
})
// AHK-style modifiers also supported
hotkeys.bind('^k', () => {
openCommandPalette() // ctrl+k
})
// Plugin grouping
hotkeys.registerPlugin('docs', {
'ctrl+f': openSearch,
'escape': closeSearch,
})Routing Model
When multiple handlers match the same hotkey:
- Highest
prioritywins - If equal priority → newest binding wins
This makes modal overrides simple:
hotkeys.bind('escape', closeModal, null, {
priority: 100,
preventDefault: true,
})API
bind(hotkey, handler, plugin?, options?)
Register a hotkey.
Returns an off() function.
const off = hotkeys.bind('mod+k', openPalette)
// Later
off()Options
{
preventDefault?: boolean
stopPropagation?: boolean
stopImmediatePropagation?: boolean
repeat?: boolean // default false on keydown
once?: boolean
when?: (event) => boolean
allowIn?: (event) => boolean
priority?: number // default 0
}Examples
Ignore repeat keydown (default behavior):
hotkeys.bind('j', nextItem)Allow inside inputs:
hotkeys.bind('mod+k', openPalette, null, {
allowIn: () => true,
preventDefault: true,
})Conditional binding:
hotkeys.bind('delete', deleteItem, null, {
when: () => selectionCount() > 0,
})Run once:
hotkeys.bind('ctrl+s', saveDraft, null, { once: true })unbind(hotkey, handler?)
Remove bindings.
hotkeys.unbind('ctrl+k')
hotkeys.unbind('ctrl+k', openPalette)registerPlugin(name, map)
Batch register hotkeys under a plugin namespace.
const unregister = hotkeys.registerPlugin('files', {
'mod+o': openFile,
'delete': deleteFile,
})
// Later
unregister()Plugin cleanup is isolated — removing one plugin never affects other bindings.
unregisterPlugin(name)
Remove all hotkeys associated with a plugin.
pause() / resume()
Temporarily disable or re-enable all routing.
ignoreInput(boolean = true)
By default, hotkeys do not fire inside:
<input><textarea><select>[contenteditable]role="textbox"
Override per-binding with allowIn().
init(options?)
Manually attach listeners.
hotkeys.init({
target: window,
capture: false,
})Auto-initializes on window by default (browser environments).
destroy()
Removes all listeners and clears internal state.
trigger(hotkey, options?)
Programmatically trigger a hotkey. Useful for testing.
hotkeys.trigger('ctrl+k')Returns true if a handler ran.
Supported Syntax
Standard
ctrl+kshift+actrl+k upmod+s(meta on macOS, ctrl elsewhere)ctrl++orctrl+plus
AHK-Style Prefix Modifiers
^k→ctrl+k!k→alt+k+k→shift+k#k→meta+k^!k→ctrl+alt+k
Modifiers must appear before the base key.
Aliases Supported
Modifiers
ctrl,control,⌃shift,⇧,+alt,option,⌥,!meta,cmd,command,win,⌘,#mod(meta on macOS, ctrl elsewhere)
Navigation / Special Keys
escape,escenter,returnspacetabbackspacedelete,delhome,endpageup,pguppagedown,pgdnup,down,left,rightf1–f19
Keys are case-insensitive.
What This Is Not
- Not a keycode polyfill
- Not a legacy browser shim
- Not a global scope manager
- Not a VSCode-style sequence engine
This is a small, deterministic routing layer for modern applications.
Browser Support
Modern browsers supporting:
KeyboardEvent.keyMapaddEventListener
Chrome, Firefox, Safari, Edge.
License
See LICENSE file for details.
