Boring Rails

Adding keyboard shortcuts and hotkeys to StimulusJS

Jul 11th, 2022 5 min read
Keyboard shortcut support for actions is now built-in to Stimulus 3.2!

Keyboard shortcuts are a great way to level up your user experience and improve the accessibility of your web applications. Even if you aren’t ready for advanced hotkey schemes, small things like binding the Escape key to close a modal can have a big impact.

Stimulus doesn’t come with built-in support for hotkeys. As part of a recent project at Arrows, I evaluated the ecosystem and wanted to share my thoughts.

stimulus-hotkeys

This package provides a hotkeys controller and uses a JSON object to map keyboard shortcuts into Stimulus actions.

<div
  data-controller="hotkeys"
  data-hotkeys-bindings-value='{"ctrl+z, command+z": "#foo->editor#undo"}'
></div>

<div id="foo" data-controller="editor"></div>

The syntax of the bindings map is similar to the Stimulus action syntax, with the addition of a preceding selector to find the Stimulus controller to invoke the action on. Internally, stimulus-hotkeys uses the more general HotKeys.JS library so it supports all kinds of fancy combinations, modifiers, and scopes.

I liked that the shortcut mapping exists in the HTML markup; this felt very much in the spirit of Hotwire. This library adds no extra code to your Stimulus controllers, which is ideal if you are combining together other third-party controllers.

Unfortunately, adding a hash-like data structure as an HTML attribute is tricky and I don’t love the JSON syntax.

stimulus-use/useHotkeys

The stimulus-use project is a collection of reusable behaviors for Stimulus. If you are familiar with React, this project is similar to React’s hooks system, but for Stimulus controllers.

Included in this collection is useHotkeys. As with stimulus-hotkeys, the heavy lifting is done by HotKeys.JS.

Here you need to define the hotkeys and respective handlers inside of your own Stimulus controllers:

import { Controller } from "@hotwired/stimulus";
import { useHotkeys } from "stimulus-use";

export default class extends Controller {
  connect() {
    useHotkeys(this, {
      "cmd+t": [this.openPalette],
      ".": [this.editFile],
    });
  }
}

This library felt right at home in a Stimulus controller and it handles unregistering the shortcuts when a controller is disconnected automatically. It was super clear what hotkeys were configured and this felt like a great option for adding hotkeys to your own application controllers.

One downside that was specific to our needs was that I had a controller that was always on the page, but should not respond to hotkeys when hidden. While useHotkey handles the Stimulus controller lifecycle, it does make it inflexible when it comes to binding and unbinding the keyboard events.

HotKeys.JS (directly)

After exploring the two most popular Stimulus-specific solutions and realizing they both used the same underlying dependency, I decided to go straight to the source.

import { Controller } from "@hotwired/stimulus";
import hotkeys from "hotkeys-js";

export default class extends Controller {
  connect() {
    hotkeys("esc", () => this.doSomething());
  }

  disconnect() {
    hotkeys.unbind("esc");
  }
}

Adding the library directly is straightforward. You do need to be careful to clean up your event listeners by calling unbind or else you’ll end up with multiple instances of the hotkey functions getting created.

For very simple cases (one or two basic hotkeys), this approach is perfectly valid. In our application, I ended up going this route as I needed more fine-grained control of when the shortcuts were bound/unbound.

github/hotkey

The last option I evaluated was a small library from GitHub called github/hotkey. It is not Stimulus / Hotwire specific.

<button data-hotkey="Shift+?">Show help dialog</button>

<a href="/page/2" data-hotkey="j">Next</a>
<a href="/help" data-hotkey="Control+h">Help</a>
<a href="/rails/rails" data-hotkey="g c">Code</a>
<a href="/search" data-hotkey="s,/">Search</a>

I found this library via the sourcemaps of HEY.com and it has been battle-tested by GitHub in the main Rails app (from the README: “This is used on almost every page on GitHub.”).

The data-hotkey attribute has the cleanest syntax of any of the options. You would want a wrapper Stimulus controller to handle lifecycle events, but it would be only for calling install and uninstall.

The design of this library relies more heavily on browser default behaviors. Instead of calling arbitrary JavaScript functions (or Stimulus actions), github/hotkey triggers the action on the target HTML element: links would trigger a navigation visit, buttons would get submitted, input fields would get focus, etc.

If you can structure your markup to use native browser functionality, this is a great option. But there isn’t a way to hook into your existing Stimulus actions directly.

Wrap it up

It really depends on what your specific hotkey needs are:

  • stimulus-hotkeys is a drop-in tool that doesn’t require any extra JavaScript
  • stimulus-use/useHotKeys is a convenient Stimulus-native API wrapper around HotKey.js
  • HotKey.js is a powerful abstraction around key events that you can mix into your own code
  • github/hotkey leans heavily on native browser behavior and has the nicest syntax

Ultimately, you’ll need to navigate the trade-offs for what you’re trying to build. In my case, I went with directly using HotKey.js because it best fit my specific requirements.

Was this article valuable? Subscribe to the low-volume, high-signal newsletter. No spam. All killer, no filler.