0.0
No release in over 3 years
Add keyboard shortcuts to Rails without writing JavaScript. Define hotkeys in views, handle navigation, requests, and DOM changes.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Development

>= 2.5
~> 13.0

Runtime

 Project Readme

Lazy Hotkeys for Lazy People

Gem Version License: MIT

Define hotkeys in ERB. Handle navigation, requests, and DOM changes without writing JS.

Install

1. Add the gem:

bundle add lazy_hotkeys

Or add to your Gemfile:

gem "lazy_hotkeys", "~> 0.1"

Then run bundle install

2. Run the generator:

rails generate lazy_hotkeys:install

This copies lazy_hotkeys.js to app/javascript/

3. Load it:

Importmap:

pin "lazy_hotkeys", to: "lazy_hotkeys.js"

Then import it:

// app/javascript/application.js
import "lazy_hotkeys"

esbuild/rollup/webpack or Vite:

Import with relative path in your entry point:

// app/javascript/application.js  (or entrypoints/application.js in Vite)
import "./lazy_hotkeys"

Done. Hotkeys work now. (hopefully)


1. Hotkeys

Press keys. Things happen. Stop pretending you like writing JavaScript.

Navigate Somewhere

<%= hotkey("g i", visit: "/inbox") %>
<%= hotkey("ctrl+n", visit: new_post_path) %>

Press g then i. You're in your inbox. Magic? No. Just less suffering.

Send a Request

<%= hotkey("ctrl+s", to: "/save", method: :post) %>
<%= hotkey("ctrl+d", to: post_path(@post), method: :delete) %>

Ctrl+S sends a POST. Ctrl+D deletes. Your mouse is crying. Good.

Dispatch an Action

<%= hotkey("ctrl+k", action: "open-command-palette") %>
// Some Stimulus controller, somewhere
document.addEventListener('lazy-hotkeys:action', (e) => {
  if (e.detail.action === 'open-command-palette') {
    this.open();
  }
});

For when you absolutely must write JavaScript.

Chain Multiple Actions

<%= hotkey("ctrl+s", chain: [
  { type: "dom", target: "#status", set_text: "Saving..." },
  { type: "request", to: "/save", method: "post" },
  { type: "dom", target: "#status", set_text: "Saved!" }
]) %>

One key. Multiple actions. Sequential.


2. DOM Manipulation

Show/Hide Things

<%= hotkey("?", dom: { target: "#help", toggle_class: "hidden" }) %>
<%= hotkey("esc", dom: { target: ".modal", add_class: "hidden" }) %>
<%= hotkey("ctrl+h", dom: { target: "#sidebar", remove_class: "collapsed" }) %>

Change Text

<%= hotkey("ctrl+shift+c", dom: {
  target: "#counter",
  set_text: "9999"
}) %>

Click Things

<%= hotkey("ctrl+enter", dom: { target: "form button", click: true }) %>
<%= hotkey("/", dom: { target: "#search-input", focus: true }) %>

Your hands never leave the keyboard. Your mouse collects dust. Evolution.

Set Attributes

<%= hotkey("ctrl+d", dom: { target: "#mode", set_attr: { "data-theme": "dark" }}) %>

Use a Template (For more complex HTML)

<%= hotkey("ctrl+p", dom: { target: "#preview", replace_with: "template" }) do %>
  <div class="preview-panel">
    <h3>Preview</h3>
    <p>Your content here</p>
  </div>
<% end %>

Press the key. The template replaces the target. No innerHTML. No XSS. Just works.


Options

Option Description
visit: "/path" Navigate to URL (Turbo or regular)
to: "/endpoint" Send request (GET/POST/PATCH/DELETE)
method: :post HTTP method (with to:)
params: { ... } Request params (with to:)
action: "name" Dispatch custom event
dom: { ... } Manipulate DOM (see below)
chain: [...] Multiple actions in sequence
scope: "#form" Only works when element exists
prevent_default: false Allow browser default (default: true)
same_origin: false Allow cross-origin (default: true)
hint: "Save" Tooltip/hint text

DOM Options

Option Description Example
target: "#id" CSS selector Required
click: true Click the element { target: "button", click: true }
focus: true Focus the element { target: "input", focus: true }
set_text: "..." Change text (safe) { target: "#status", set_text: "Done" }
add_class: "..." Add CSS class { target: "#box", add_class: "active" }
remove_class: "..." Remove CSS class { target: "#box", remove_class: "hidden" }
toggle_class: "..." Toggle CSS class { target: "#menu", toggle_class: "open" }
set_attr: { k: v } Set attributes (whitelisted) { target: "#mode", set_attr: { "data-theme": "dark" } }
remove_attr: "..." Remove attribute { target: "#input", remove_attr: "disabled" }
replace_with: "template" Replace with template content See template example above

Cross-Origin

By default, requests to other domains are blocked:

<%# This works %>
<%= hotkey("ctrl+s", to: "/save") %>

<%# This is blocked %>
<%= hotkey("ctrl+x", to: "https://github.com/Plan-Vert/open-letter") %>

<%# This works (you asked for it) %>
<%= hotkey("?", visit: "https://discord.gg/BUtwjJTwxt", same_origin: false) %>

javascript:, data:, and file: URLs are always blocked, even with same_origin: false.

Attribute whitelist: Check lazy_hotkeys.js and adjust attribute whitelist to your needs, define only what you need.

Config

Minimal config at the top of lazy_hotkeys.js:

const CFG = {
    normalizeCmdToCtrl: true, // Cmd on Mac = Ctrl on Windows
    skipInInputs: true, // Don't fire when typing in inputs
    sequenceTimeoutMs: 800 // How long to wait for sequences like "g i"
};

Change these values directly in the file. That's it.


Requirements

Rails 5.1+ (needs tag.template helper)


License

MIT