StimulusJS Keyboard Hotkeys (Keyboard navigation)
Some apps let you click keyboard buttons or combinations to quickly navigate around.
Example 1:
Example 2:
Goal: If a user presses a hotkey combination on a keyboard, trigger a click on a link, button or element.
In our example a user will always have to click ⌘ Command
+ yourKey
(Mac), or Ctrl
+ yourKey
(Linux/Windows).
2025 Update #
rails g stimulus click
// app/javascript/controllers/click_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
click(event) {
const formElements = ['INPUT', 'TEXTAREA', 'SELECT']
if (formElements.includes(event.target.tagName)) {
return
}
this.element.click()
}
}
<!-- clicking "n" will click the link -->
<a href="/posts/new" data-controller="click" data-action="keydown.n@window->click#click">New Post</a>
<!-- clicking "Ctrl+K" will click the button -->
<button data-controller="click" data-action="keydown.meta+k@window->click#click keydown.ctrl+k@window->click#click">Search</button>
<!-- clicking "Ctrl+Enter" will click the button -->
<button data-controller="click" data-action="keydown.cmd+enter@window->click#click keydown.ctrl+enter@window->click#click">Submit</button>
Option 1: With event listeners #
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="hotkeys"
export default class extends Controller {
static targets = ["button"]
connect() {
document.addEventListener('keydown', this.handleKeydown.bind(this))
}
disconnect() {
document.removeEventListener('keydown', this.handleKeydown.bind(this))
}
handleKeydown(event) {
// meta for Mac, ctrl for Linux/Windows
let pressedCtrl = event.metaKey || event.ctrlKey
let pressedKey = event.key
if (pressedCtrl) {
// find a buttonTarget that has hotkey set to the pressed key
let buttonTarget = this.buttonTargets.find((el) => el.dataset.hotkey == pressedKey)
if (buttonTarget) {
event.preventDefault()
buttonTarget.focus()
buttonTarget.click()
}
}
}
}
HTML usage example:
<body data-controller="hotkeys">
<a href="#" data-hotkeys-target="button" data-hotkey="e">Edit</a>
<button data-hotkeys-target="button" data-hotkey="s">Save</button>
<button data-hotkeys-target="button" data-hotkey="d">Delete</button>
</body>
Option 2 (better): with Stimulus actions #
Instead of EventListeners, we can use Stimulus KeyboardEvent Filter
-<body data-controller="hotkeys">
+<body data-controller="hotkeys" data-action="keydown->hotkeys#handleKeydown">
<a href="#" data-hotkeys-target="button" data-hotkey="e">Edit</a>
<button data-hotkeys-target="button" data-hotkey="s">Save</button>
<button data-hotkeys-target="button" data-hotkey="d">Delete</button>
</body>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "button" ]
handleKeydown(event) {
// Check if Cmd (Mac) or Ctrl (Windows/Linux) is pressed simultaneously with the key
let pressedCtrl = event.metaKey || event.ctrlKey
let pressedKey = event.key.toLowerCase()
if (pressedCtrl) {
// find a button with data-hotkey attribute that matches the pressed key
let buttonTarget = this.buttonTargets.find((el) => el.dataset.hotkey == pressedKey)
if (buttonTarget) {
event.preventDefault();
buttonTarget.focus()
buttonTarget.click()
}
}
}
}
Important considerations:
- Most
Ctrl+yourKey
combination are reserved by the browser. Different browsers can have different reserved combinations. A few characters that seem to work across browsers arek
,u
,b
,k
. - This brings value only to users with keyboards (not mobile devices)
- Using accesskeys can have accessibility issues
That’s it! 🤠
Did you like this article? Did it save you some time?