KBD & Hotkey Rails Components
KBD & Hotkey components for your Ruby on Rails application.
Installation
1. Stimulus Controller Setup
Start by adding the following controllers to your project:
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
click(event) {
if (this.#isClickable && !this.#shouldIgnore(event)) {
// Try to prevent default behavior (won't work for all system shortcuts)
event.preventDefault();
event.stopPropagation();
this.element.click();
// Focus the element to trigger the tooltip (if it's focusable)
if (typeof this.element.focus === "function") {
this.element.focus();
}
}
}
#shouldIgnore(event) {
return (
event.defaultPrevented || event.target.closest("input, textarea, [contenteditable], trix-editor, .trix-dialog")
);
}
get #isClickable() {
return getComputedStyle(this.element).pointerEvents !== "none";
}
}
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["mac", "nonMac"];
connect() {
this.detectOS();
}
detectOS() {
const isMac = this.#isMac();
// Show/hide mac-specific elements
this.macTargets.forEach((el) => {
el.classList.toggle("hidden", !isMac);
});
// Show/hide non-mac elements
this.nonMacTargets.forEach((el) => {
el.classList.toggle("hidden", isMac);
});
}
#isMac() {
// Use modern userAgentData API when available
if (navigator.userAgentData && navigator.userAgentData.platform) {
return navigator.userAgentData.platform === "macOS";
}
// Fallback to userAgent detection
return /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
}
}
2. Custom CSS
Here are the custom CSS classes that we used on Rails Blocks to style the kbd components.
kbd {
@apply items-center justify-center rounded-md border border-black/10 bg-white px-1 font-mono text-[11px] text-neutral-800 shadow-[0px_1.5px_0px_0px_rgba(0,0,0,0.05)] dark:border-white/10 dark:bg-neutral-900 dark:text-neutral-200 dark:shadow-[0px_1px_0px_0px_rgba(255,255,255,0.1)];
}
Examples
Basic KBD
A basic kbd component that just renders a keyboard key. This can be used to display a keyboard shortcut or hotkey. This is purely cosmetic and does not handle any keyboard events.
<div class="flex flex-col justify-center items-center gap-2">
<kbd>
A
</kbd>
<div class="flex items-center gap-1">
<kbd>
Ctrl
</kbd>
<span>+</span>
<kbd>
A
</kbd>
</div>
</div>
Button with Hotkey
A kbd component that renders a keyboard key and a hotkey. This can be used to display a keyboard shortcut or hotkey. This is purely cosmetic and does not handle any keyboard events.
⌥ Option combos that work on macOS:
⌥ Option combos using letters are blocked by macOS because they create special characters:
<div class="flex flex-col justify-center items-center gap-2">
<button class="flex flex-wrap items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip hotkey"
data-tooltip-content="Clicked!"
data-tooltip-placement-value="top"
data-tooltip-trigger-value="focus"
data-action="keydown.ctrl+b@document->hotkey#click"
>
Click me by pressing <kbd>Ctrl</kbd>+<kbd>B</kbd>
</button>
<a href="https://www.google.com/search?q=rails+blocks"
target="_blank"
class="flex flex-wrap items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="hotkey"
data-action="keydown.shift+g@document->hotkey#click">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="32" height="32" viewBox="0 0 32 32"><title>Google</title><g fill="none"><path d="M16.2856 13.4546V18.8764H23.974C23.6364 20.62 22.6233 22.0964 21.1038 23.0891L25.7402 26.6146C28.4415 24.1711 30 20.582 30 16.3184C30 15.3257 29.9091 14.371 29.7402 13.4547L16.2856 13.4546Z" fill="#4285F4"></path><path d="M8.27956 18.6646L7.23387 19.449L3.53247 22.2744C5.88314 26.8434 10.701 29.9998 16.2855 29.9998C20.1425 29.9998 23.3763 28.7525 25.74 26.6144L21.1036 23.089C19.8309 23.9289 18.2075 24.4381 16.2855 24.4381C12.5711 24.4381 9.41536 21.9817 8.2854 18.6726L8.27956 18.6646Z" fill="#34A853"></path><path d="M3.53237 9.72559C2.55839 11.6091 2 13.7346 2 16C2 18.2654 2.55839 20.3909 3.53237 22.2745C3.53237 22.2871 8.28576 18.6599 8.28576 18.6599C8.00004 17.8199 7.83116 16.9291 7.83116 15.9999C7.83116 15.0707 8.00004 14.1798 8.28576 13.3398L3.53237 9.72559Z" fill="#FBBC05"></path><path d="M16.2858 7.57452C18.3897 7.57452 20.2598 8.28723 21.7533 9.66179L25.8443 5.65276C23.3637 3.38735 20.143 2 16.2858 2C10.7013 2 5.88314 5.14362 3.53247 9.72544L8.28571 13.34C9.41552 10.0309 12.5714 7.57452 16.2858 7.57452Z" fill="#EA4335"></path></g></svg>
Open Google by pressing <kbd>Shift</kbd>+<kbd>G</kbd>
</a>
<button class="flex flex-wrap items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="hotkey"
data-action="keydown.meta+k@document->hotkey#click keydown.ctrl+k@document->hotkey#click"
onclick="alert('Search triggered!')">
Search with <kbd>⌘</kbd>+<kbd>K</kbd> or <kbd>Ctrl</kbd>+<kbd>K</kbd>
</button>
<button class="flex flex-wrap items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="hotkey"
data-action="keydown.meta+shift+p@document->hotkey#click keydown.ctrl+shift+p@document->hotkey#click"
onclick="alert('Command palette opened!')">
Command Palette with <kbd>⌘</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd> or <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>
</button>
<button class="flex flex-wrap items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="hotkey"
data-action="keydown.ctrl+enter@document->hotkey#click keydown.meta+enter@document->hotkey#click"
onclick="alert('Form submitted!')">
Submit with <kbd>⌘</kbd>+<kbd>Enter</kbd> or <kbd>Ctrl</kbd>+<kbd>Enter</kbd>
</button>
<!-- Option key examples that WORK on macOS -->
<div class="w-full max-w-md border-t border-neutral-200 dark:border-neutral-700 pt-2 mt-2">
<p class="text-sm text-neutral-600 dark:text-neutral-400 mb-2 text-center"><kbd>⌥</kbd> Option combos that work on macOS:</p>
</div>
<button class="flex flex-wrap items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="hotkey"
data-action="keydown.alt+enter@document->hotkey#click"
onclick="alert('Alternative action!')">
Works: <kbd>Option</kbd>+<kbd>Enter</kbd>
</button>
<button class="flex flex-wrap items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="hotkey"
data-action="keydown.alt+tab@document->hotkey#click"
onclick="alert('Tab action!')">
Works: <kbd>Option</kbd>+<kbd>Tab</kbd>
</button>
<!-- Option key examples that DON'T work on macOS -->
<div class="w-full max-w-md border-t border-neutral-200 dark:border-neutral-700 pt-2 mt-2">
<p class="text-sm text-neutral-600 dark:text-neutral-400 mb-2 text-center"><kbd>⌥</kbd> Option combos using letters are blocked by macOS because they create special characters:</p>
</div>
<button class="flex flex-wrap items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200 opacity-50"
data-controller="hotkey"
data-action="keydown.alt+n@document->hotkey#click"
disabled
onclick="alert('This won\'t work!')">
Blocked: <kbd>Option</kbd>+<kbd>N</kbd> (creates ˜)
</button>
<button class="flex flex-wrap items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200 opacity-50"
data-controller="hotkey"
data-action="keydown.alt+e@document->hotkey#click"
disabled
onclick="alert('This won\'t work!')">
Blocked: <kbd>Option</kbd>+<kbd>E</kbd> (creates ´)
</button>
</div>
OS-specific KBD
KBD components that are shown or hidden based on the user's OS.
<div class="flex flex-col justify-center items-center gap-4" data-controller="os-detect">
<!-- Example 1: Save shortcut -->
<div class="flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
<span>Save document:</span>
<div class="flex items-center gap-1">
<kbd data-os-detect-target="mac" class="hidden">⌘</kbd>
<kbd data-os-detect-target="nonMac" class="hidden">Ctrl</kbd>
<span>+</span>
<kbd>S</kbd>
</div>
</div>
<!-- Example 2: Copy shortcut -->
<div class="flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
<span>Copy selection:</span>
<div class="flex items-center gap-1">
<kbd data-os-detect-target="mac" class="hidden">⌘</kbd>
<kbd data-os-detect-target="nonMac" class="hidden">Ctrl</kbd>
<span>+</span>
<kbd>C</kbd>
</div>
</div>
<!-- Example 3: Undo shortcut -->
<div class="flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
<span>Undo action:</span>
<div class="flex items-center gap-1">
<kbd data-os-detect-target="mac" class="hidden">⌘</kbd>
<kbd data-os-detect-target="nonMac" class="hidden">Ctrl</kbd>
<span>+</span>
<kbd>Z</kbd>
</div>
</div>
<!-- Example 4: Multiple modifier keys -->
<div class="flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
<span>Force quit:</span>
<div class="flex items-center gap-1">
<div data-os-detect-target="mac" class="hidden flex items-center gap-1">
<kbd>⌘</kbd>
<span>+</span>
<kbd>⌥</kbd>
<span>+</span>
<kbd>Q</kbd>
</div>
<div data-os-detect-target="nonMac" class="hidden flex items-center gap-1">
<kbd>Ctrl</kbd>
<span>+</span>
<kbd>Alt</kbd>
<span>+</span>
<kbd>Q</kbd>
</div>
</div>
</div>
<!-- Example 6: Interactive button with OS-specific shortcut -->
<button class="flex flex-wrap items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip hotkey"
data-tooltip-content="Action performed!"
data-tooltip-placement-value="top"
data-tooltip-trigger-value="focus"
data-action="keydown.meta+shift+o@document->hotkey#click keydown.ctrl+shift+o@document->hotkey#click">
Open command palette with
<kbd data-os-detect-target="mac" class="hidden">⌘</kbd>
<kbd data-os-detect-target="nonMac" class="hidden">Ctrl</kbd>
<span>+</span>
<kbd>Shift</kbd>
<span>+</span>
<kbd>O</kbd>
</button>
</div>
Configuration
Hotkey Controller
The hotkey controller enables keyboard shortcuts for any clickable element (buttons, links, etc.). It automatically handles preventing default behavior and ignores shortcuts when the user is typing in inputs or textareas.
Basic Usage
Add the controller and keyboard event action to any element:
<button data-controller="hotkey"
data-action="keydown.ctrl+s@document->hotkey#click">
Save with <kbd>Ctrl</kbd>+<kbd>S</kbd>
</button>
Keyboard Event Format
The hotkey controller uses Stimulus's built-in keyboard event format:
keydown.[modifiers]+[key]@[scope]->[controller]#[action]
Examples:
keydown.ctrl+s@document->hotkey#click # Ctrl+S
keydown.meta+k@document->hotkey#click # Cmd+K (Mac) or Win+K
keydown.shift+enter@document->hotkey#click # Shift+Enter
keydown.alt+tab@document->hotkey#click # Alt+Tab
Available Modifiers
ctrl
- Control keymeta
- Command key on Mac, Windows key on PCalt
- Alt key (Option on Mac)shift
- Shift key
Multiple Platform Support
To support both Mac and PC shortcuts, add multiple actions:
<button data-controller="hotkey"
data-action="keydown.meta+k@document->hotkey#click
keydown.ctrl+k@document->hotkey#click">
Search with <kbd>⌘</kbd>+<kbd>K</kbd> or <kbd>Ctrl</kbd>+<kbd>K</kbd>
</button>
⚠️ macOS Option Key Limitation
Many Option (Alt) key combinations with letters don't work on macOS because they create special characters. For example:
- Option+N creates ˜ (tilde)
- Option+E creates ´ (accent)
- Option+U creates ¨ (umlaut)
Stick to Option with non-letter keys like Enter, Tab, or arrow keys for reliable cross-platform support.
OS Detect Controller
The OS detect controller shows or hides elements based on the user's operating system. Perfect for displaying platform-specific keyboard shortcuts.
Basic Usage
Add the controller to a container and use targets to show/hide elements:
<div data-controller="os-detect">
<kbd data-os-detect-target="mac" class="hidden">⌘</kbd>
<kbd data-os-detect-target="nonMac" class="hidden">Ctrl</kbd>
</div>
Available Targets
mac
- Shown on macOS devicesnonMac
- Shown on non-macOS devices (Windows, Linux, etc.)
Detection Method
The controller uses modern browser APIs when available:
- Checks
navigator.userAgentData.platform
(preferred) - Falls back to
navigator.userAgent
regex matching - Detects Mac, iPhone, iPad, and iPod as "Mac" platforms
Common Patterns
Form Submission Shortcut
<form data-controller="hotkey os-detect"
data-action="keydown.meta+enter@document->hotkey#click
keydown.ctrl+enter@document->hotkey#click">
<!-- form fields -->
<button type="submit">
Submit with
<span data-os-detect-target="mac" class="hidden">
<kbd>⌘</kbd>+<kbd>Enter</kbd>
</span>
<span data-os-detect-target="nonMac" class="hidden">
<kbd>Ctrl</kbd>+<kbd>Enter</kbd>
</span>
</button>
</form>
Command Palette
<button data-controller="hotkey os-detect"
data-action="keydown.meta+k@document->hotkey#click
keydown.ctrl+k@document->hotkey#click"
onclick="openCommandPalette()">
<span data-os-detect-target="mac" class="hidden">
Press <kbd>⌘</kbd>+<kbd>K</kbd>
</span>
<span data-os-detect-target="nonMac" class="hidden">
Press <kbd>Ctrl</kbd>+<kbd>K</kbd>
</span>
</button>
Best Practices
- Always provide visual indication of keyboard shortcuts in your UI
- Use platform-appropriate shortcuts (⌘ on Mac, Ctrl on PC)
- Test shortcuts don't conflict with browser or system shortcuts
- Provide alternative ways to trigger actions for accessibility
- Consider using the tooltip component to show feedback when shortcuts are used
- Remember that some combinations may be reserved by the OS or browser