import { Controller } from "@hotwired/stimulus"
import Mousetrap from "mousetrap"
export default class extends Controller {
connect() {
// Global shortcuts
Mousetrap.bind('/', () => this.focusSearch())
Mousetrap.bind('c', () => this.compose())
Mousetrap.bind('?', () => this.showHelp())
Mousetrap.bind('g h', () => this.goHome())
Mousetrap.bind('g p', () => this.goPosts())
// Vim-style navigation
Mousetrap.bind('j', () => this.nextItem())
Mousetrap.bind('k', () => this.previousItem())
}
disconnect() {
Mousetrap.reset()
}
focusSearch() {
const searchInput = document.querySelector('[data-search-input]')
if (searchInput) {
searchInput.focus()
searchInput.select()
return false // Prevent default
}
}
compose() {
const composeButton = document.querySelector('[data-compose-button]')
if (composeButton) {
composeButton.click()
return false
}
}
showHelp() {
const helpButton = document.querySelector('[data-help-button]')
if (helpButton) {
helpButton.click()
return false
}
}
goHome() {
window.location.href = '/'
return false
}
goPosts() {
window.location.href = '/posts'
return false
}
nextItem() {
const items = document.querySelectorAll('[data-item]')
const current = document.activeElement.closest('[data-item]')
if (!current && items.length > 0) {
items[0].focus()
} else {
const currentIndex = Array.from(items).indexOf(current)
const next = items[currentIndex + 1]
if (next) next.focus()
}
return false
}
previousItem() {
const items = document.querySelectorAll('[data-item]')
const current = document.activeElement.closest('[data-item]')
if (current) {
const currentIndex = Array.from(items).indexOf(current)
const prev = items[currentIndex - 1]
if (prev) prev.focus()
}
return false
}
}
<!DOCTYPE html>
<html>
<head>
<title>MyApp</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application" %>
<%= javascript_importmap_tags %>
</head>
<body data-controller="keyboard-shortcuts">
<%= yield %>
<!-- Keyboard shortcuts help modal -->
<div data-controller="modal">
<button type="button"
data-help-button
data-action="modal#open"
class="hidden">Help</button>
<div data-modal-target="container" class="hidden">
<div data-modal-target="dialog" role="dialog">
<h2>Keyboard Shortcuts</h2>
<dl class="shortcuts-list">
<dt><kbd>/</kbd></dt>
<dd>Focus search</dd>
<dt><kbd>c</kbd></dt>
<dd>Compose new post</dd>
<dt><kbd>g</kbd> then <kbd>h</kbd></dt>
<dd>Go to home</dd>
<dt><kbd>g</kbd> then <kbd>p</kbd></dt>
<dd>Go to posts</dd>
<dt><kbd>j</kbd> / <kbd>k</kbd></dt>
<dd>Next / Previous item</dd>
<dt><kbd>?</kbd></dt>
<dd>Show this help</dd>
</dl>
</div>
</div>
</div>
</body>
</html>
Power users appreciate keyboard shortcuts that speed up common actions. I integrate the Mousetrap library via Stimulus to define app-wide shortcuts like cmd+k for search, c to compose, or ? to show help. The controller binds shortcuts on connect and unbinds on disconnect to avoid memory leaks. I'm careful about context—some shortcuts only work on certain pages, and I disable them when typing in inputs. A help modal triggered by ? documents available shortcuts. This pattern significantly improves productivity for frequent users while remaining invisible to those who don't use shortcuts. I also show keyboard hints in tooltips as progressive disclosure.