Keyboard
@ailuracode/alpine-keyboard
Headless scoped keyboard shortcut registry with chords, sequences, conflict resolution, and platform-aware mod normalization.
Install
pnpm add @ailuracode/alpine-keyboard @ailuracode/alpine-core alpinejsQuick start
import Alpine from "alpinejs";import { keyboardPlugin } from "@ailuracode/alpine-keyboard";
Alpine.plugin( keyboardPlugin({ pauseWhileScopesActive: ["modal"], shortcuts: [ { shortcut: "mod+k", handler: () => openCommandPalette(), options: { id: "command-palette", metadata: { label: "Open command palette", group: "Navigation" }, }, }, { shortcut: "g h", handler: () => navigate("/"), options: { id: "go-home", metadata: { label: "Go home" } }, }, ], }));
Alpine.start();<div x-data x-init=" $keyboard.activateScope('editor'); $keyboard.register('mod+s', () => save(), { scope: 'editor', id: 'save' }); "> <p x-text="$keyboard.commands.map((c) => c.label).join(', ')"></p></div>Store / magic API
keyboardPlugin() registers $store.keyboard and $keyboard (same reactive object).
| Member | Description |
|---|---|
commands |
Readonly shortcut metadata for discovery UIs |
activeScopes |
Currently active scope names |
suspendedScopes |
Temporarily disabled scopes |
register(shortcut, handler, options?) |
Register a shortcut; returns disposer |
unregister(id) |
Remove a shortcut by id |
activateScope(scope) |
Enable shortcuts bound to a scope |
deactivateScope(scope) |
Disable a scope |
suspendScope(scope) |
Pause a scope without deactivating |
resumeScope(scope) |
Resume a suspended scope |
handleKeydown(event) |
Dispatch a keyboard event manually |
Controller API
import { createKeyboard } from "@ailuracode/alpine-keyboard";
const keyboard = createKeyboard({ sequenceTimeout: 1000, pauseWhileScopesActive: ["modal"],});
const dispose = keyboard.register("escape", (event) => { closeDialog();}, { scope: "modal", priority: 10 });
keyboard.activateScope("modal");dispose();keyboard.destroy();Shortcut syntax
| Pattern | Meaning |
|---|---|
mod+k |
meta+k on macOS, ctrl+k elsewhere |
ctrl+shift+p |
Chord with explicit modifiers |
g h |
Two-key sequence (1 s timeout by default) |
escape |
Named keys and aliases (esc, space, arrows) |
Accessibility
- Expose
commandsmetadata in a host-built help panel — this package does not render UI. - Prefer chords over single-key global bindings.
- Use scoped shortcuts inside overlays and configure
pauseWhileScopesActive. - Listen for
conflictevents while authoring shortcuts.
Plugin
import Alpine from "alpinejs";import { keyboardPlugin } from "@ailuracode/alpine-keyboard";
Alpine.plugin( keyboardPlugin({ pauseWhileScopesActive: ["modal"], shortcuts: [ { shortcut: "mod+/", handler: () => showShortcutHelp(), options: { id: "shortcut-help", metadata: { label: "Show keyboard shortcuts", group: "Help", }, }, }, ], }));Registers $store.keyboard and $keyboard.
Avoiding name collisions
If your application already owns a $store.keyboard or another toolkit plugin registers on that name, rename the integration surface without touching the controller:
Alpine.plugin( keyboardPlugin({ storeKey: "hotkeys", // → $store.hotkeys // magicKey follows storeKey by default → $hotkeys magicKey: "keys", // explicit override → $keys }));storeKey is the only argument most hosts need. magicKey moves independently only when both names must be freed. The exposed constants DEFAULT_KEYBOARD_STORE_KEY and DEFAULT_KEYBOARD_MAGIC_KEY keep the rename discoverable from TypeScript.
Scopes
Scopes gate which shortcuts are eligible. The default scope is global.
$keyboard.activateScope("editor");$keyboard.register("mod+s", save, { scope: "editor", id: "save" });$keyboard.suspendScope("editor"); // temporary pause$keyboard.deactivateScope("editor");Configure pauseWhileScopesActive: ["modal"] on the controller to pause only-global shortcuts while modal scopes are active.
Sequences
Space-separated tokens define multi-key sequences:
keyboard.register("g h", () => goHome(), { id: "go-home" });Sequences reset after sequenceTimeout (default 1000 ms) or when an unexpected key is pressed.
Conflicts
Registrations with the same chord in the same scope emit a conflict event. At runtime the highest priority handler wins.
Editable targets
By default shortcuts do not fire when focus is inside input, textarea, select, or contenteditable elements. Pass allowInEditable: true for editor-specific bindings.
Standalone controller
import { createKeyboard } from "@ailuracode/alpine-keyboard";
const keyboard = createKeyboard();const dispose = keyboard.register("mod+k", handler);keyboard.mount(); // idempotent — attaches one window listenerkeyboard.destroy(); // removes listeners and clears registrationsdispose();Non-goals
- Replacing
x-on:keydownfor local element behavior - Rendering shortcut help or command palette UI (see
@ailuracode/alpine-command)
License
MIT
