Adding keyboard shortcuts and hotkeys to StimulusJS
Tags: ruby, rails, stimulus
Published: 2022-07-12 02:04:10

---
title: Adding keyboard shortcuts and hotkeys to StimulusJS
published: true
date: 2022-07-11 13:00:00 UTC
tags: ruby,rails,stimulus
canonical_url: https://boringrails.com/articles/stimulus-hotkeys-keyboard-shortcuts/
---

 This post is part of [Hotwire Summer](/hotwire-summer): a new season of content on Boring Rails!

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](https://arrows.to/), I evaluated the ecosystem and wanted to share my thoughts.

## stimulus-hotkeys

[This package](https://github.com/leastbad/stimulus-hotkeys) provides a `hotkeys` controller and uses a JSON object to map keyboard shortcuts into Stimulus actions.

```html
<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](https://wangchujiang.com/hotkeys/) 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](https://github.com/stimulus-use/stimulus-use/) 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](https://github.com/stimulus-use/stimulus-use/blob/main/docs/use-hotkeys.md). As with `stimulus-hotkeys`, the heavy lifting is done by [HotKeys.JS](https://wangchujiang.com/hotkeys/).

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

```js
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 be on the page, but should not respond to hotkeys when hidden. While `useHotkey`s 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](https://wangchujiang.com/hotkeys/).

```js
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](https://github.com/github/hotkey). It is not Stimulus / Hotwire specific.

```html
<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](https://www.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.

* * *
[![](https://boringrails.com/images/twitter-banner.png)](https://twitter.com/_swanson)