hotkeys-js: a dependency-free keyboard shortcut library for the browser
➷ A robust Javascript library for capturing keyboard input. It has no dependencies.
At a glance
- What is it?
- hotkeys-js is a small TypeScript library that binds key combinations to callbacks, scopes and filters. It installs from npm, ships UMD and ESM builds, and is MIT licensed.
- Who is it for?
- Adopt hotkeys-js when you need keyboard shortcuts in a browser app without pulling in a framework binding, and when you want the same API in plain JavaScript, React or Vue through the library's own exports. Do not adopt it if you need shortcuts inside a native desktop shell, a terminal, or a canvas widget that manages its own focus model, because the library listens to document-level keyboard events.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 21 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 27, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem hotkeys-js solves, and who actually needs it
Binding a keyboard shortcut by hand is a small job that turns into a large one. You add a keydown listener, compare event.key or event.keyCode, remember that modifier keys report differently across platforms, decide whether a shortcut should fire while focus sits in a text input, and then repeat all of it for the next shortcut. hotkeys-js exists to collapse that work into one call. The package.json describes it as "A simple micro-library for defining and dispatching keyboard shortcuts. It has no dependencies." That last clause is the selling point: the library has no runtime dependencies, so it does not drag a framework or a utility belt into your bundle.
The audience is anyone building a browser interface with a keyboard-driven workflow. Editors, dashboards, media players, data tables, drawing tools, and internal admin screens all accumulate shortcuts. The library also matters to teams that already use React or Vue but do not want a framework-specific wrapper, because the same hotkeys() function is exported for every environment through the package's exports map. If you only need one shortcut on one page, a raw addEventListener is still the smaller answer.
How hotkeys-js binds keys, scopes and filters
The mechanism is a single document-level listener plus a registry of bindings. You call hotkeys() with a key string and a handler, and the library keeps that pair in an internal map. When a keyboard event reaches the document, the library normalises the combination, matches it against the registry, and invokes the handlers that apply. Key strings compose modifiers with a plus sign, so a combination such as ctrl+shift+a is parsed rather than compared character by character.
Two features carry most of the design weight. The first is scoping. A binding can be assigned to a named scope, and only bindings in the active scope fire. That is what lets a modal own its own keys while the modal is open without the underlying page reacting to the same strokes. The second is filtering. A filter function decides whether a binding is allowed to run at a given moment, which is how you suppress shortcuts while the user is typing into an input or a contenteditable region. Both mechanisms live in the same registry, so there is no separate event plumbing to maintain.
The repository layout supports this reading. src/ holds the TypeScript source, test/ holds the test suite, and dist/ holds the built artifacts. The build runs through vite, with a separate website/ directory that builds the documentation site. The package exposes four entry points: dist/hotkeys-js.js for import, dist/hotkeys-js.umd.cjs for require, dist/hotkeys-js.min.js for the browser field, and dist/index.d.ts for types. That split is deliberate and worth knowing before you debug a bundling problem.
Installing hotkeys-js and binding your first shortcut
The package installs from npm under the name hotkeys-js. According to the repository's package.json, the version at the time of writing is 4.0.8 and the licence is MIT. The package.json also declares the install scripts the project uses: prepare runs the library build and husky install, build:lib runs vite build, and test runs jest with coverage. A CDN build also exists, since the package publishes a browser field pointing at dist/hotkeys-js.min.js.
The package.json files array ships only two directories, dist and doc, so the published tarball contains the built library and the documentation rather than the src/ tree. That matters if you plan to read the source from node_modules: you will find dist/index.d.ts for the type surface and the compiled bundles, not the TypeScript files.
To install it, run the package manager command against the published name:
npm install hotkeys-jsAfter installation, the entry points resolve through the exports map in package.json. An import statement picks up dist/hotkeys-js.js, a require call picks up dist/hotkeys-js.umd.cjs, and a browser field consumer picks up dist/hotkeys-js.min.js. Types come from dist/index.d.ts. If your bundler complains about the module format, that exports map is the first place to look, because the four targets are separate files rather than one build with a wrapper.
The README's examples for binding, scoping and filtering are the authority on the call signatures, and the website/ directory builds the documentation site that carries them. Read those before writing your first binding, since the key-string syntax and the scope argument order are what the library parses.
Where hotkeys-js gets in the way
The document-level listener is the main constraint. Because the library attaches at the document and matches events against a global registry, it does not know about component lifecycles. If a component registers a binding and is then unmounted without an explicit unbind, that binding stays in the registry and keeps firing. In a long-lived single-page application this is a real leak, and the symptom is a shortcut that triggers a handler for a view the user left ten minutes ago. The library gives you unbind for this, but the discipline is yours.
Focus is the second sharp edge. A shortcut bound to a single letter will fire while the user is typing in a text field unless you add a filter or check the event target yourself. The filter mechanism exists precisely for this, and the README's examples show it, but it is opt-in. Teams that skip it ship an app where typing the letter j in a search box jumps to the next row.
Browser and operating system shortcuts are the third limit. Combinations that the browser or the OS has already claimed may never reach your page, and preventDefault only helps when the event does arrive. There is no way for a JavaScript library to take back a shortcut the host environment owns. Finally, this is a browser library. If your target is a terminal, a native desktop shell, or a canvas application with its own input model, you are solving a different problem and should look elsewhere.
hotkeys-js compared with Mousetrap and React bindings
Mousetrap is the closest historical alternative, and the difference is in the API surface and the packaging. Mousetrap also binds document-level keyboard events and also has no dependencies, but its API is built around a chained bind() call and a static object rather than an exported function with a scope registry. If your codebase already speaks Mousetrap, migrating to hotkeys-js means rewriting every binding site, not swapping an import.
Framework-specific packages such as react-hotkeys-hook take a different approach again. They wrap the same underlying problem in a hook so that the binding is tied to the component's lifetime and is cleaned up automatically on unmount. That solves the leak described above by construction. The trade-off is that the binding is now only usable inside a React component tree, and the dependency graph grows. hotkeys-js keeps one API that works in plain JavaScript, React and Vue alike, at the cost of asking you to unbind explicitly.
A third option is to write the listener yourself. For one or two shortcuts on a page that never unmounts, roughly fifteen lines of addEventListener code will do the job with no dependency at all. hotkeys-js earns its place once you have more than a handful of combinations, more than one scope, or more than one framework in the same repository.
Maintenance, licence and what upgrading costs
The repository is not archived, and the last push was on 2026-09-09. The most recent release listed is v4.0.8 on the same date, with v4.0.7 and v4.0.6 both on 2026-08-28. That release cadence suggests fixes are still landing, though the version numbers stay in the 4.x line, which is consistent with a library whose API has settled.
The licence is MIT, stated in package.json and present as a LICENSE file at the repository root. MIT permits commercial use, modification and redistribution provided the copyright notice and permission notice are retained. That is the general shape of the licence, not legal advice; if your organisation has a policy on third-party notices, the LICENSE file is what your process needs to pick up.
Upgrade cost is low but not zero. The package ships separate import, require and browser entry points through its exports map, so a major version could change which file your bundler picks up even if the hotkeys() signature is unchanged. The build pipeline runs type-check, then the library build, then the documentation build, then lint, and the test script invokes the build first. If you vendor a fork, you inherit that toolchain: vite, eslint and jest. The dist/ directory is committed to the repository, which means a fork that skips the build step can still ship, but you would be shipping whatever was last built.
Editorial conclusion
Adopt hotkeys-js when you need keyboard shortcuts in a browser app without pulling in a framework binding, and when you want the same API in plain JavaScript, React or Vue through the library's own exports. Do not adopt it if you need shortcuts inside a native desktop shell, a terminal, or a canvas widget that manages its own focus model, because the library listens to document-level keyboard events. Before committing, verify two things in your own codebase: that the keys you pick do not collide with browser or OS shortcuts, and that your bundler resolves the exports map in package.json to the build you expect, since the package ships separate import, require and browser entries.
Frequently asked questions
How do I install hotkeys-js?
Install it from npm with npm install hotkeys-js. The package publishes ESM, UMD and minified browser builds, so it works with a bundler or directly in the browser through the browser field in package.json.
How do I use hotkeys-js to bind a keyboard shortcut?
Import the default export and call hotkeys with a key string and a callback, as the README's examples show. The callback receives the original event, so you can call preventDefault to stop the browser's own handling.
Does hotkeys-js have any dependencies?
No. The package.json description states that it has no dependencies, and the package ships its own builds through dist/.
Can I limit hotkeys-js shortcuts to one part of the page?
Yes, through scopes. You assign a binding to a named scope and activate it with hotkeys.setScope, so only bindings in the active scope fire. The library does not switch the scope back for you when a view closes.
What happens if I forget to unbind a hotkeys-js shortcut?
The binding stays in the library's registry and keeps matching keyboard events, because the listener is attached at the document rather than to a component. The visible symptom is a shortcut that still fires after the view that registered it is gone.
Can hotkeys-js override a shortcut the browser already uses?
Only if the event reaches the page. Combinations the browser or operating system has claimed may never be delivered, and preventDefault has no effect on an event that the page never receives.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/jaywcjlove-hotkeys-js)