hotkeys-js vs keymaster vs mousetrap
Keyboard Shortcut Libraries for Modern Web Applications
hotkeys-jskeymastermousetrap

Keyboard Shortcut Libraries for Modern Web Applications

hotkeys-js, keymaster, and mousetrap are JavaScript libraries designed to simplify the implementation of keyboard shortcuts in web applications. They abstract away the complexities of cross-browser key event handling, modifier key detection, and key combination parsing. While mousetrap has historically been the industry standard for its robustness and plugin ecosystem, hotkeys-js has emerged as a modern, dependency-free alternative with TypeScript support. keymaster, once popular for its simplicity, is now considered legacy software with significant maintenance gaps.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
hotkeys-js07,1164.58 MB1613 months agoMIT
keymaster06,515-8212 years ago-
mousetrap011,780-2346 years agoApache-2.0 WITH LLVM-exception

Keyboard Shortcut Libraries: hotkeys-js vs mousetrap vs keymaster

Handling keyboard input in the browser is notoriously difficult. You have to deal with inconsistent key codes across browsers, the difference between keypress and keydown, modifier key states (Shift, Ctrl, Alt, Meta), and preventing default browser actions without breaking accessibility. Libraries like hotkeys-js, mousetrap, and keymaster exist to solve these problems, but they are not all created equal.

Let's break down how these three libraries handle real-world engineering challenges.

🛑 Maintenance Status: Active vs Legacy

Before writing a single line of code, you must consider the lifecycle of the dependency.

keymaster is effectively abandoned. The repository has seen no meaningful updates in years. It lacks support for modern ES6 modules without transpilation hacks and has known issues with memory leaks where event listeners are not properly cleaned up. Using it today introduces unnecessary risk.

mousetrap is in maintenance mode. It is stable and widely used, but feature development has slowed. It works perfectly for standard use cases but doesn't embrace modern TypeScript workflows natively.

hotkeys-js is actively maintained. It is built with modern tooling, has first-class TypeScript support, and regularly addresses issues related to new browser versions and edge cases in key event handling.

🎯 Basic Binding Syntax

All three libraries aim to make binding shortcuts simple, but the syntax and flexibility differ.

hotkeys-js uses a clean, chainable API that supports multiple keys in a single string.

import hotkeys from 'hotkeys-js';

// Bind single key
hotkeys('a', (event, handler) => {
  console.log('You pressed a!');
});

// Bind combination
hotkeys('ctrl+shift+a', (event, handler) => {
  console.log('Complex combo triggered');
});

mousetrap uses a similar string-based approach but relies on a global Mousetrap object.

import Mousetrap from 'mousetrap';

// Bind single key
Mousetrap.bind('a', () => {
  console.log('You pressed a!');
});

// Bind combination
Mousetrap.bind(['ctrl+shift+a', 'meta+shift+a'], () => {
  console.log('Complex combo triggered');
});

keymaster uses a very similar syntax but lacks the robustness in parsing complex combinations found in the others.

import key from 'keymaster';

// Bind single key
key('a', () => {
  console.log('You pressed a!');
});

// Bind combination
key('ctrl+shift+a', () => {
  console.log('Complex combo triggered');
});

🔭 Scoping: Managing Contexts

In complex applications (like a graphic editor or a data grid), shortcuts often need to change based on the current view. For example, pressing "Delete" should remove a file in the file browser but delete a character in a text input.

hotkeys-js has built-in, robust scope support. You can define scopes and switch between them instantly.

import hotkeys from 'hotkeys-js';

// Define scope
hotkeys.setScope('inputs');

// This shortcut only works in 'inputs' scope
hotkeys('enter', { scope: 'inputs' }, () => {
  submitForm();
});

// Switch scope globally
hotkeys.setScope('navigation');

mousetrap supports scopes via its core API, allowing you to bind keys to specific named contexts.

import Mousetrap from 'mousetrap';

// Bind to specific scope
Mousetrap.bind('enter', submitForm, 'inputs');

// Switch active scope
Mousetrap.setScope('navigation');

keymaster also supports scopes, but the implementation is less flexible when dealing with rapid context switching in modern reactive frameworks.

import key from 'keymaster';

// Set scope
key.setScope('inputs');

// Bind within current scope
key('enter', submitForm);

// Change scope
key.setScope('navigation');

⌨️ Handling Input Fields

A common pain point is preventing shortcuts from firing when the user is typing in a text field. You usually want "Ctrl+S" to save everywhere, but "S" alone should just type the letter "s" in an input.

hotkeys-js provides a filter function that you can customize. By default, it ignores most keys when typing in inputs, but allows explicit overrides.

import hotkeys from 'hotkeys-js';

// Customize filter to allow 'esc' even in inputs
hotkeys.filter = (event) => {
  const target = event.target || event.srcElement;
  const { tagName } = target;
  // Allow Esc in inputs, block others
  if (event.key === 'Escape') return true;
  
  const flag = !target.isContentEditable && 
    tagName !== 'INPUT' && 
    tagName !== 'SELECT' && 
    tagName !== 'TEXTAREA';
  return flag;
};

mousetrap has a similar filter mechanism built-in that checks for input fields automatically.

import Mousetrap from 'mousetrap';

// Override default filter
Mousetrap.addKeyCallback('esc', (e) => {
  // This will fire even inside an input because we bound it explicitly
  closeModal();
}, 'keydown');

// Or customize the global filter
Mousetrap.stopCallback = (e, element) => {
  if (e.key === 'Escape') return false; // Don't stop Esc
  return element.classList.contains('no-shortcuts');
};

keymaster requires manual checks or reliance on its basic filter, which is often too rigid for complex forms.

import key from 'keymaster';

// Manual check required for fine-grained control
key('esc', () => {
  const active = document.activeElement;
  if (active && active.tagName === 'INPUT') return;
  closeModal();
});

🧩 Extensibility and Plugins

Sometimes you need more than just key presses. You might need to record a sequence of keys (like "g then d" to delete) or handle special media keys.

mousetrap shines here with a mature plugin ecosystem. The record plugin allows for complex sequences out of the box.

import Mousetrap from 'mousetrap';
import 'mousetrap-plugins/global-bind';
import 'mousetrap-plugins/record';

// Record a sequence
Mousetrap.record((sequence) => {
  console.log('User typed:', sequence);
});

// Global bindings (requires extension)
Mousetrap.bindGlobal('ctrl+shift+h', showHelp);

hotkeys-js keeps things lightweight. It supports sequences natively without heavy plugins, but lacks the extensive third-party addon library of Mousetrap.

import hotkeys from 'hotkeys-js';

// Native sequence support
hotkeys('g, d', () => {
  console.log('Sequence g then d detected');
});

keymaster has minimal plugin support and struggles with complex sequences compared to the other two.

import key from 'keymaster';

// Basic sequence support exists but is less reliable
key('g, d', () => {
  console.log('Sequence detected');
});

📊 Summary of Technical Differences

Featurehotkeys-jsmousetrapkeymaster
Maintenance✅ Active⚠️ Stable/Legacy❌ Abandoned
TypeScript✅ Native Types⚠️ Community Types❌ None
Dependencies✅ Zero✅ Zero✅ Zero
Scoping✅ Robust API✅ Supported⚠️ Basic
Sequences✅ Native✅ Via Plugins⚠️ Limited
Bundle Size🟢 Very Small🟢 Small🟢 Small
Browser SupportModern + IE11All (including old)Old/Modern

💡 Final Recommendation

For new projects, hotkeys-js is the clear winner. It offers the best balance of modern developer experience (TypeScript, ES modules), active maintenance, and a small footprint. Its API is intuitive and handles the tricky edge cases of browser key events without the baggage of legacy code.

Stick with mousetrap only if you are maintaining an existing application that already uses it, or if you absolutely require one of its specific plugins for key recording that you don't want to reimplement. It is a solid library, but it shows its age in the details.

Avoid keymaster entirely. The lack of maintenance and known issues make it a liability in any professional codebase. Migrating away from it should be a priority if you encounter it in legacy systems.

How to Choose: hotkeys-js vs keymaster vs mousetrap

  • hotkeys-js:

    Choose hotkeys-js for new projects requiring TypeScript support, zero dependencies, and active maintenance. It is ideal for modern SPAs (React, Vue, Svelte) where you need reliable scope management and a small footprint without polyfills. Its API is clean and handles edge cases like key repetition and modifier logic better than older libraries.

  • keymaster:

    Do NOT choose keymaster for any new production project. It is effectively deprecated, with no significant updates in nearly a decade, known memory leak issues regarding event listeners, and poor support for modern browser behaviors. Migrating to hotkeys-js or mousetrap is strongly recommended to ensure security and stability.

  • mousetrap:

    Choose mousetrap if you are maintaining a legacy codebase that already relies on it or if you specifically need its extensive plugin ecosystem (like recording sequences or global shortcuts via extensions). It remains a stable choice for broad browser compatibility, including very old browsers, but lacks native TypeScript definitions and modern build tooling optimizations.

README for hotkeys-js

Using my app is also a way to support me:
Scap: Screenshot & Markup Edit Screen Test Deskmark Keyzer Vidwall Hub VidCrop Vidwall Mousio Hint Mousio Musicer Audioer FileSentinel FocusCursor Videoer KeyClicker DayBar Iconed Menuist Quick RSS Quick RSS Web Serve Copybook Generator DevTutor for SwiftUI RegexMate Time Passage Iconize Folder Textsound Saver Create Custom Symbols DevHub Resume Revise Palette Genius Symbol Scribe

Hotkeys

Buy me a coffee Follow On X GitHub Actions CI Coverage Status Chinese jaywcjlove/hotkeys-js

HotKeys.js is an input capture library with some very special features, it is easy to pick up and use, has a reasonable footprint (~8kB) (gzipped: 3.8kB), and has no dependencies. It should not interfere with any JavaScript libraries or frameworks. Official document demo preview, compatibility test. More examples.

╭┈┈╮          ╭┈┈╮  ╭┈┈╮
┆  ├┈┈..┈┈┈┈┈.┆  └┈╮┆  ├┈┈..┈┈┈┈┈..┈┈.┈┈..┈┈┈┈┈.
┆     ┆┆  □  ┆┆   ┈┤┆    < ┆  -__┘┆  ┆  ┆┆__ ┈┈┤
╰┈┈┴┈┈╯╰┈┈┈┈┈╯╰┈┈┈┈╯╰┈┈┴┈┈╯╰┈┈┈┈┈╯╰┈┈┈  ┆╰┈┈┈┈┈╯
                                  ╰┈┈┈┈┈╯

Usage

You will need Node.js installed on your system.

npm install hotkeys-js --save
import hotkeys from 'hotkeys-js';

hotkeys('f5', function(event, handler){
  // Prevent the default refresh event under WINDOWS system
  event.preventDefault()
  alert('you pressed F5!')
});

Browser Usage

Or manually download and link hotkeys.js in your HTML. The library provides different formats for different use cases:

CDN Links: UNPKG | jsDelivr | Githack | Statically

Available Formats:

IIFE (Immediately Invoked Function Expression) - Recommended for direct browser usage:

<script src="https://unpkg.com/hotkeys-js/dist/hotkeys-js.min.js">
</script>
<script type="text/javascript">
hotkeys('ctrl+a,ctrl+b,r,f', function (event, handler){
  switch (handler.key) {
    case 'ctrl+a': alert('you pressed ctrl+a!');
      break;
    case 'ctrl+b': alert('you pressed ctrl+b!');
      break;
    case 'r': alert('you pressed r!');
      break;
    case 'f': alert('you pressed f!');
      break;
    default: alert(event);
  }
});
</script>

UMD (Universal Module Definition) - For CommonJS/AMD environments:

<script src="https://unpkg.com/hotkeys-js/dist/hotkeys-js.umd.cjs">
</script>

ES Module - For modern browsers with module support:

<script type="module">
import hotkeys from 'https://unpkg.com/hotkeys-js/dist/hotkeys-js.js';
hotkeys('ctrl+a', function(event, handler){
  alert('you pressed ctrl+a!');
});
</script>

Used in React

react-hotkeys is the React component that listen to keydown and keyup keyboard events, defining and dispatching keyboard shortcuts. Detailed use method please see its documentation react-hotkeys.

react-hotkeys-hook - React hook for using keyboard shortcuts in components. Make sure that you have at least version 16.8 of react and react-dom installed, or otherwise hooks won't work for you.

Browser Support

Hotkeys.js has been tested and should work in.

Internet Explorer 6+
Safari
Firefox
Chrome

Supported Keys

HotKeys understands the following modifiers: , shift, option, , alt, ctrl, control, command, and .

The following special keys can be used for shortcuts: backspace, tab, clear, enter, return, esc, escape, space, up, down, left, right, home, end, pageup, pagedown, del, delete, f1 through f19, num_0 through num_9, num_multiply, num_add, num_enter, num_subtract, num_decimal, num_divide.

Command()
Control
Option(alt)
Shift
Caps Lock(Capital)
fn Does not support fn
↩︎ return/Enter space

Defining Shortcuts

One global method is exposed, key which defines shortcuts when called directly.

declare interface HotkeysInterface extends HotkeysAPI {
  (key: string, method: KeyHandler): void;
  (key: string, scope: string, method: KeyHandler): void;
  (key: string, option: HotkeysOptions, method: KeyHandler): void;
  shift?: boolean;
  ctrl?: boolean;
  alt?: boolean;
  option?: boolean;
  control?: boolean;
  cmd?: boolean;
  command?: boolean;
}
declare interface HotkeysAPI {
  setScope: SetScope;
  getScope: GetScope;
  deleteScope: DeleteScope;
  getPressedKeyCodes: GetPressedKeyCodes;
  getPressedKeyString: GetPressedKeyString;
  getAllKeyCodes: GetAllKeyCodes;
  isPressed: IsPressed;
  filter: Filter;
  trigger: Trigger;
  unbind: Unbind;
  noConflict: NoConflict;
  keyMap: Record<string, number>;
  modifier: Record<string, number>;
  modifierMap: Record<string | number, number | string>;
}
hotkeys('f5', function(event, handler) {
  // Prevent the default refresh event under WINDOWS system
  event.preventDefault();
  alert('you pressed F5!');
});

// Returning false stops the event and prevents default browser events
// Mac OS system defines `command + r` as a refresh shortcut
hotkeys('ctrl+r, command+r', function() {
  alert('stopped reload!');
  return false;
});

// Single key
hotkeys('a', function(event,handler){
  //event.srcElement: input
  //event.target: input
  if(event.target === "input"){
      alert('you pressed a!')
  }
  alert('you pressed a!')
});

// Key Combination
hotkeys('ctrl+a,ctrl+b,r,f', function (event, handler){
  switch (handler.key) {
    case 'ctrl+a': alert('you pressed ctrl+a!');
      break;
    case 'ctrl+b': alert('you pressed ctrl+b!');
      break;
    case 'r': alert('you pressed r!');
      break;
    case 'f': alert('you pressed f!');
      break;
    default: alert(event);
  }
});

hotkeys('ctrl+a+s', function() {
    alert('you pressed ctrl+a+s!');
});

// Using a scope
hotkeys('*','wcj', function(event){
  console.log('do something', event);
});

option

  • scope<String>: Sets the scope in which the shortcut key is active
  • element<HTMLElement>: Specifies the DOM element to bind the event to
  • keyup<Boolean>: Whether to trigger the shortcut on key release
  • keydown<Boolean>: Whether to trigger the shortcut on key press
  • splitKey<String>: Delimiter for key combinations (default is +)
  • capture<Boolean>: Whether to trigger the listener during the capture phase (before the event bubbles down)
  • single<Boolean>: Allows only one callback function (automatically unbinds previous one)
hotkeys('o, enter', {
  scope: 'wcj',
  element: document.getElementById('wrapper'),
}, function() {
  console.log('do something else');
});

hotkeys('ctrl-+', { splitKey: '-' }, function(e) {
  console.log('you pressed ctrl and +');
});

hotkeys('+', { splitKey: '-' }, function(e){
  console.log('you pressed +');
})

keyup

key down and key up both perform callback events.

hotkeys('ctrl+a,alt+a+s', {keyup: true}, function(event, handler) {
  if (event.type === 'keydown') {
    console.log('keydown:', event.type, handler, handler.key);
  }

  if (event.type === 'keyup') {
    console.log('keyup:', event.type, handler, handler.key);
  }
});

API REFERENCE

Asterisk "*"

Modifier key judgments

hotkeys('*', function() {
  if (hotkeys.shift) {
    console.log('shift is pressed!');
  }

  if (hotkeys.ctrl) {
    console.log('ctrl is pressed!');
  }

  if (hotkeys.alt) {
    console.log('alt is pressed!');
  }

  if (hotkeys.option) {
    console.log('option is pressed!');
  }

  if (hotkeys.control) {
    console.log('control is pressed!');
  }

  if (hotkeys.cmd) {
    console.log('cmd is pressed!');
  }

  if (hotkeys.command) {
    console.log('command is pressed!');
  }
});

setScope

Use the hotkeys.setScope method to set scope. There can only be one active scope besides 'all'. By default 'all' is always active.

// Define shortcuts with a scope
hotkeys('ctrl+o, ctrl+alt+enter', 'issues', function() {
  console.log('do something');
});
hotkeys('o, enter', 'files', function() {
  console.log('do something else');
});

// Set the scope (only 'all' and 'issues' shortcuts will be honored)
hotkeys.setScope('issues'); // default scope is 'all'

getScope

Use the hotkeys.getScope method to get scope.

hotkeys.getScope();

deleteScope

Use the hotkeys.deleteScope method to delete a scope. This will also remove all associated hotkeys with it.

hotkeys.deleteScope('issues');

You can use second argument, if need set new scope after deleting.

hotkeys.deleteScope('issues', 'newScopeName');

unbind

Similar to defining shortcuts, they can be unbound using hotkeys.unbind.

// unbind 'a' handler
hotkeys.unbind('a');

// Unbind a hotkeys only for a single scope
// If no scope is specified it defaults to the current 
// scope (hotkeys.getScope())
hotkeys.unbind('o, enter', 'issues');
hotkeys.unbind('o, enter', 'files');

Unbind events through functions.

function example() {
  hotkeys('a', example);
  hotkeys.unbind('a', example);

  hotkeys('a', 'issues', example);
  hotkeys.unbind('a', 'issues', example);
}

To unbind everything.

hotkeys.unbind();

isPressed

For example, hotkeys.isPressed(77) is true if the M key is currently pressed.

hotkeys('a', function() {
  console.log(hotkeys.isPressed('a')); //=> true
  console.log(hotkeys.isPressed('A')); //=> true
  console.log(hotkeys.isPressed(65)); //=> true
});

trigger

trigger shortcut key event

hotkeys.trigger('ctrl+o');
hotkeys.trigger('ctrl+o', 'scope2');

getPressedKeyCodes

Returns an array of key codes currently pressed.

hotkeys('command+ctrl+shift+a,f', function() {
  console.log(hotkeys.getPressedKeyCodes()); //=> [17, 65] or [70]
})

getPressedKeyString

Returns an array of key codes currently pressed.

hotkeys('command+ctrl+shift+a,f', function() {
  console.log(hotkeys.getPressedKeyString()); 
  //=> ['⌘', '⌃', '⇧', 'A', 'F']
})

getAllKeyCodes

Get a list of all registration codes.

hotkeys('command+ctrl+shift+a,f', function() {
  console.log(hotkeys.getAllKeyCodes());
  // [
  //   { 
  //      scope: 'all', 
  //      shortcut: 'command+ctrl+shift+a', 
  //      mods: [91, 17, 16], 
  //      keys: [91, 17, 16, 65] 
  //    },
  //   { scope: 'all', shortcut: 'f', mods: [], keys: [42] }
  // ]
})

filter

By default hotkeys are not enabled for INPUT SELECT TEXTAREA elements. Hotkeys.filter to return to the true shortcut keys set to play a role, false shortcut keys set up failure.

hotkeys.filter = function(event){
  return true;
}
// How to add the filter to edit labels. 
// <div contentEditable="true"></div>
// "contentEditable" Older browsers that do not support drops
hotkeys.filter = function(event) {
  var target = event.target || event.srcElement;
  var tagName = target.tagName;
  return !(
    target.isContentEditable || 
    tagName == 'INPUT' || 
    tagName == 'SELECT' || 
    tagName == 'TEXTAREA'
  );
}

hotkeys.filter = function(event){
  var tagName = (event.target || event.srcElement).tagName;
  hotkeys.setScope(
    /^(INPUT|TEXTAREA|SELECT)$/.test(tagName) ? 'input' : 'other'
  );
  return true;
}

noConflict

Relinquish HotKeys’s control of the hotkeys variable.

var k = hotkeys.noConflict();
k('a', function() {
  console.log("do something")
});

hotkeys()
// -->Uncaught TypeError: hotkeys is not a function(anonymous function)
// @ VM2170:2InjectedScript._evaluateOn
// @ VM2165:883InjectedScript._evaluateAndWrap
// @ VM2165:816InjectedScript.evaluate @ VM2165:682

Development

To develop, Install dependencies, Get the code:

$ git https://github.com/jaywcjlove/hotkeys.git
$ cd hotkeys     # Into the directory
$ npm install    # or  yarn install

To develop, run the self-reloading build:

$ npm run watch

Run Document Website Environment.

# Generate documentation website
$ npm run doc 
# Live-generate documentation website
$ npm run start 

To contribute, please fork Hotkeys.js, add your patch and tests for it (in the test/ folder) and submit a pull request.

$ npm run test
$ npm run test:watch # Development model

Contributors

As always, thanks to our amazing contributors!

Made with action-contributors.

Special thanks to @dimensi for the refactoring of version 4.0.

License

MIT © Kenny Wong