| examples | ||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
hotkey-accel-js
Framework-agnostic hotkey bindings over UI components, with visual overlays that tell the user which keys do what. Works with vanilla JS, React and Next.js (client components).
Features
- Bind hotkeys to DOM components via
bind(),bind_by(), or adata-hotkeyattribute - Two interaction modes:
- hold — press modifier(s) + key together (
Ctrl+K) - press — tap the modifier, then press the key within
pressTimeoutms
- hold — press modifier(s) + key together (
- Configurable modifier:
ctrl,alt,shift,meta, or combos likectrl+alt - Rebind policy:
override(last binding wins) orignore(conflict reported viaonError) - Optional overlay on the bound component, shown while the modifier is held (hold mode) or during the press window (press mode)
- Event hooks (
bind,rebind,unbind,trigger,error), mutable runtime config
Installation
npm install hotkey-accel-js
Vanilla JS
<script src="dist/hotkey-accel.iife.js"></script>
<script>
const manager = new HotkeyAccel.HotkeyManager({
modifier: 'ctrl',
mode: 'hold',
showOverlay: true,
})
manager.bind('s', 'save-btn', document.getElementById('save'), () => {
console.log('saved')
})
manager.init() // scans [data-hotkey] elements, clicks them on trigger
</script>
ES modules:
import { HotkeyManager, getDefaultManager } from 'hotkey-accel-js'
const manager = new HotkeyManager({
modifier: 'ctrl+alt', // or 'ctrl' / 'alt' / 'shift' / 'meta'
mode: 'press', // 'press' | 'hold'
pressTimeout: 800, // press-mode window in ms
showOverlay: true, // render overlay hints
rebind: 'override', // 'override' | 'ignore'
})
const result = manager.bind('k', 'comp-1', document.getElementById('btn'), onSuccess, onError)
if (result.ok) console.log('bound', result.binding.comboKey)
manager.bind_by('.toolbar > button', 't', onSuccess) // or omit key to read data-hotkey per node
const results = manager.init() // bind all [data-hotkey] elements, default action clicks them
API
new HotkeyManager(options)
| option | default | description |
|---|---|---|
modifier |
'ctrl' |
'ctrl', 'alt', 'shift', 'meta', or 'ctrl+alt' |
mode |
'hold' |
'hold' (modifier+key together) or 'press' (tap then key) |
pressTimeout |
1000 |
press-mode window in ms |
showOverlay |
true |
show overlay hints on bound components |
rebind |
'override' |
'override' replaces the previous binding, 'ignore' keeps it |
overlayClass |
'hk-overlay' |
extra/custom class for overlay elements |
overlayStyle |
null |
inline style object merged onto overlay elements |
injectOverlayCSS |
true |
inject default overlay styles |
preventDefault |
true |
call preventDefault() when a hotkey matches |
clickOnDataHotkey |
true |
init()/bind_by() bound nodes get clicked as default action |
warnReserved |
true |
warn via onError/error when binding a browser-owned combo |
manager.bind(key, componentId, node, onSuccess?, onError?)
Binds key (e.g. 'k', 'ctrl+k', 'arrowup') to a DOM node under a component id. Returns { ok, binding } or { ok: false, error }. Without an explicit modifier in key, the instance modifier is applied. onError receives { error, key, componentId, existing? } for hotkey_taken, invalid_hotkey, invalid_node, modifier_mismatch.
manager.bind_by(selector, key?, onSuccess?, onError?)
Binds every matching node. Omit key to read data-hotkey per node. Returns an array of bind results.
manager.init(scope?)
Scans scope (default document) for [data-hotkey] elements and binds them.
manager.reload(scope?)
Re-scans scope (default document) for [data-hotkey] elements and (re)binds them, then re-declares every component-bound hotkey (HotkeyTarget / useHotkey). Useful for SPAs that add/remove hotkey-declaring elements dynamically or when a modal temporarily overrides a component hotkey — reload() restores anything declarative. Existing programmatic bind() calls are left untouched, and conflicts are always resolved with override semantics, regardless of the rebind config.
manager.bindComponent(key, componentId, node, onSuccess?, onError?)
Same as bind(), but records the binding so reload() re-declares it. Used internally by the React adapter.
manager.undeclareComponent(componentId)
Forgets all declared component bindings for a component id, so reload() no longer restores them.
manager.unbind(key?, componentId?)
Unbinds by key, by component id, or everything. Returns the number of removed bindings.
Runtime config
setModifier(m), setMode(mode), setPressTimeout(ms), setShowOverlay(bool), setRebind(mode), dispose(), list().
Events
manager.on('bind' | 'rebind' | 'unbind' | 'trigger' | 'error', fn) — returns an unsubscribe function.
data-hotkey
<button data-hotkey="s">Save</button>
<button data-hotkey="ctrl+shift+d">Dark mode</button>
Use manager.init() to pick them up. data-hotkey accepts plain keys (instance modifier applied) or explicit combos.
React / Next.js
'use client'
import { HotkeyProvider, useHotkey, HotkeyTarget } from 'hotkey-accel-js/react'
export default function App() {
return (
<HotkeyProvider options={{ modifier: 'ctrl', mode: 'hold' }}>
<Toolbar />
</HotkeyProvider>
)
}
function Toolbar() {
const ref = useRef(null)
useHotkey('s', 'save-btn', ref, () => console.log('saved'))
return (
<div ref={ref}>Save</div>
<HotkeyTarget hotkey="d" componentId="dark-btn" onSuccess={() => console.log('dark')}>
<span>Dark mode</span>
</HotkeyTarget>
)
}
useHotkeyManager(options)— returns the nearestHotkeyProvidermanager or the module-level default manageruseHotkey(key, componentId, ref, onSuccess?, onError?)— binds on mount, unbinds on unmount<HotkeyTarget hotkey="d" componentId="id" onSuccess={...}>— wrapper element that binds itself- The module-level default manager is shared; pass
manager={...}toHotkeyTarget/useHotkeyfor a custom one
Demos
examples/basic.html— vanilla HTML/JS demo (data-hotkey, bind, bind_by, mode/modifier/overlay toggles)examples/node.mjs— headless Node demo (jsdom), run withnode examples/node.mjsexamples/hotkey-app-demo/— React (Vite) POS app: product grid, cart drawer and modal forms driven by hotkeys, including modal hotkey overrides that restore the underlying hotkeys on close. See its README for the minimal usage pattern.
Browser-owned shortcuts
Browsers own a set of shortcuts and intercept them before the page receives any key event: Ctrl+N (new window), Ctrl+T/Ctrl+W (tab), Ctrl+1–9 (tab switching), Ctrl+S, Ctrl+L, Ctrl+K, devtools combos and more. No JavaScript can make the app authoritative over those — the keydown never reaches the page.
The library flags such combos at bind time: onError is called (and an error event emitted) with error: 'browser_reserved' while the binding is still registered (it may fire in kiosk/embedded contexts). Disable the check with warnReserved: false. Prefer combos browsers leave to the page — Ctrl+Alt or Ctrl+Shift variants of the same keys are usually free.
Overlay behavior
Overlays are absolutely-positioned, non-interactive hints rendered above each bound component:
- hold mode: visible while all of the combo's modifiers are pressed, hidden on release
- press mode: visible from the modifier tap until the press window expires (or the hotkey fires)
- Overlays reposition on scroll/resize and are removed when their binding is unbound
Development
npm install
npm run build # bundles dist/ (esm, cjs, iife, react esm/cjs)
npm test # node --test with jsdom