@goboldlyforward/keycuts
v1.0.1
Published
Small, opinionated keyboard shortcuts as a drop-in plugin. Smart defaults (cmd+k search, shift+? help, esc close), conflict detection, input guard, auto-rendered help panel.
Downloads
261
Maintainers
Readme
keycuts.js
Small, opinionated keyboard shortcuts as a drop-in plugin. Ships with smart defaults (cmd+k search, shift+? help panel, esc close), conflict detection at register-time, runtime input guard, and an auto-rendered grouped help panel.
Demo
goboldlyforward.github.io/keycuts — try the defaults, register custom shortcuts, watch conflicts get caught.
What it does
Mounts a keydown listener on document, normalizes the event into a ctrl+alt+shift+meta+key combo, and runs the matching shortcut — focus an element, click it, submit a form, toggle the help panel, or run a handler you supply. Plain-key shortcuts (e, a) are skipped while someone is typing in an input. The help-panel renderer reads from the live registry and re-renders when shortcuts change.
Install
npm install @goboldlyforward/keycutsThe script is UMD, so a bundler import works (the CSS ships alongside):
import Keycuts from '@goboldlyforward/keycuts';
import '@goboldlyforward/keycuts/keycuts.css';Or drop it straight in from a CDN — no build step; the <script> exposes the Keycuts global:
<link rel="stylesheet" href="https://unpkg.com/@goboldlyforward/keycuts/keycuts.css">
<script src="https://unpkg.com/@goboldlyforward/keycuts/keycuts.js"></script>Usage
<input data-keycuts-search type="search" placeholder="Search…">
<div id="shortcuts"></div>
<script>
const cuts = new Keycuts(); // ships cmd+k, shift+?, esc
cuts.add({
id: 'new_project',
keys: 'cmd+n',
description: 'New project',
group: 'Projects',
selector: '[data-command="new-project"]',
});
cuts.mountList('#shortcuts'); // toggled by shift+?
</script>new Keycuts(options?) registers the listener immediately. Pass { defaults: false } to skip the built-in shortcuts and start from an empty registry.
Defaults
Three shortcuts ship enabled out of the box:
| Keys | Command | Default target |
| --------- | ------------------------ | --------------------------------------------------------------- |
| cmd+k | Search (focuses) | [data-keycuts-search], [data-command="search"] |
| shift+? | Show keyboard shortcuts | toggles every panel mounted with cuts.mountList(...) |
| esc | Close dialog or panel | [data-keycuts-close], [data-command="close"] |
Disable any default by id:
cuts.remove('search');
// or temporarily:
cuts.disable('search');
cuts.enable('search');Replace one in place:
cuts.replace({
id: 'search',
keys: 'cmd+/',
description: 'Search',
group: 'Navigation',
selector: '[data-command="search"]',
action: 'focus',
});Common shortcuts
A broader catalog ships in Keycuts.COMMON — cherry-pick the ones your app supports:
const cuts = new Keycuts();
cuts.add(Keycuts.COMMON.find(s => s.id === 'save'));
cuts.add(Keycuts.COMMON.find(s => s.id === 'command_palette'));| Id | Keys | Action | Default selector |
| ----------------- | ----------------- | ------- | --------------------------------------------------------------------------- |
| search | cmd+k | focus | [data-keycuts-search], [data-command="search"] |
| help | shift+? | toggle | (toggles mounted help panels) |
| close | esc | click | [data-keycuts-close], [data-command="close"] |
| command_palette | cmd+shift+p | click | [data-keycuts-command-palette], [data-command="command-palette"] |
| new_record | cmd+n | click | [data-command="new"] |
| save | cmd+s | submit | [data-command="save"] |
| edit | e | click | [data-command="edit"] |
| archive | a | click | [data-command="archive"] |
| delete | shift+backspace | click | [data-command="delete"] |
Actions
cuts.add({
id: 'reload',
keys: 'cmd+r',
description: 'Reload data',
handler: (event, shortcut) => {
// Anything you want. Return `false` to skip preventDefault.
refreshData();
},
});| Action | Behavior |
| -------------------- | --------------------------------------------------------------------------------- |
| click (default) | Looks up selector, calls .click() |
| focus | Looks up selector, calls .focus() |
| submit | Looks up selector, calls .requestSubmit() (form) or .click() (button) |
| keycuts:toggle-help| Toggles every panel mounted via mountList() |
| custom handler | Runs your function with (event, shortcut). preventDefault unless you return false |
Every action also dispatches a keycuts:invoke CustomEvent (bubbling, detail.shortcut) on the target element.
Input guard
Plain keys like e or a would steal keystrokes while someone is typing. The guard skips a shortcut whenever the event target is inside an input field:
input, textarea, select, [contenteditable="true"], [role="textbox"]Opt back in per-shortcut for combos that are always safe to fire (e.g. cmd+s to save):
cuts.add({
id: 'save',
keys: 'cmd+s',
description: 'Save',
action: 'submit',
selector: 'form[data-command="save"]',
allowInInputs: true,
});Override the selector globally on construction:
new Keycuts({
ignoreInputSelector: 'input, textarea, [data-no-shortcuts]',
});Conflict detection
add() throws if either the id or the normalized keys is already registered. The message tells you what conflicts:
cuts.add({ id: 'search', keys: 'cmd+/', ... });
// Error: Keycuts: id search already registered (was cmd+k).
// Pass { replace: true } to overwrite intentionally.Pass { replace: true } (or use cuts.replace(...)) to overwrite intentionally. addMany() is shorthand for bulk-loading with replace semantics.
Help panel
Mount on any empty element — the plugin builds grouped <section> markup inside it and re-renders whenever the registry changes:
<div id="shortcuts"></div>
<script>
const cuts = new Keycuts();
cuts.mountList('#shortcuts');
</script>shift+? toggles every mounted panel; call cuts.toggleList(), cuts.showList(), or cuts.hideList() from your own UI as well. The default styling is a centered modal with a backdrop; switch to inline rendering by adding .keycuts-shortcuts--inline to the host element.
Key labels are humanized per-platform via Keycuts.humanizeKeys(keys) — ⌘K on macOS, Ctrl+K on Windows/Linux.
Options
new Keycuts({
defaults: true, // register cmd+k, shift+?, esc
consoleWarnings: true, // warn on conflicts (errors throw regardless)
ignoreInputSelector: 'input, textarea, select, [contenteditable="true"], [role="textbox"]',
autoBind: true, // attach the keydown listener immediately
target: document, // element to attach keydown to
shortcuts: [], // additional shortcuts to register at construction
});Methods
cuts.add(shortcut, { replace: false }); // register one
cuts.addMany([ ... ]); // register many (replace semantics)
cuts.replace(shortcut); // overwrite by id-or-keys
cuts.remove(idOrKeys); // unregister
cuts.disable(idOrKeys); // keep registered, stop firing
cuts.enable(idOrKeys); // re-enable
cuts.list(); // → Array<shortcut>
cuts.grouped(); // → { groupName: Array<shortcut> }
cuts.mountList(target, { hidden: true }); // render + register a help panel
cuts.toggleList(force?); // toggle every mounted panel
cuts.showList(); cuts.hideList();
cuts.handle(event); // manually feed a KeyboardEvent
cuts.destroy(); // tear down listener + panelsStatics
Keycuts.DEFAULTS; // the three built-ins
Keycuts.COMMON; // broader catalog (cherry-pick what your app supports)
Keycuts.normalizeKeys('Cmd+K'); // → 'meta+k'
Keycuts.humanizeKeys('meta+k'); // → '⌘K' on Mac, 'Ctrl+K' elsewhereKey normalization
keys strings are parsed by splitting on + and normalizing each part. Modifier aliases:
cmd,command→metaoption,opt→altcontrol,ctl→ctrlescape→escreturn→enterspacebar→space
Modifiers always emit in ctrl+alt+shift+meta+key order, so Shift+Cmd+P and meta+shift+p resolve to the same combo. Special keys: esc, enter, space, backspace, up, down, left, right.
Custom events
Every successful invocation dispatches a bubbling CustomEvent on the target:
document.addEventListener('keycuts:invoke', (e) => {
console.log(e.detail.shortcut.id, 'fired');
});Requirements
HTML, CSS, and ~6KB of JavaScript. No framework, no build step. Uses addEventListener and CustomEvent.
Roadmap
- [x] Shortcut registry with id + keys conflict detection
- [x] Built-in defaults: search, help, close
- [x] Common-command catalog (cherry-pick into your app)
- [x] Input guard with per-shortcut
allowInInputsopt-in - [x] Built-in actions:
click,focus,submit, toggle help - [x] Custom
handlercallback per shortcut - [x] Auto-rendered help panel (modal + inline variants)
- [x] Live re-render of help panel as the registry changes
- [x] Per-platform key humanization (⌘ on Mac, Ctrl elsewhere)
- [x]
keycuts:invokeCustomEvent for observers - [x] Demo with playground + shortcuts list
- [x] Publish to npm (as
@goboldlyforward/keycuts) - [ ] Deploy demo to gh-pages
- [ ] GitHub Actions CI (eslint + stylelint)
- [ ] Key-sequence support (
g then ifor Gmail-style nav) - [ ] Scoped registries (per-page or per-modal shortcut sets)
- [ ] Optional
keycuts-railsgem wrapper
License
MIT — see LICENSE.
