Skip to content

GoBoldlyForward/keycuts

Repository files navigation

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.js — 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

For now, download keycuts.js and keycuts.css from this repo. (npm publication pending.)

<link rel="stylesheet" href="path/to/keycuts.css">
<script src="path/to/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 + panels

Statics

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' elsewhere

Key normalization

keys strings are parsed by splitting on + and normalizing each part. Modifier aliases:

  • cmd, commandmeta
  • option, optalt
  • control, ctlctrl
  • escapeesc
  • returnenter
  • spacebarspace

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

  • Shortcut registry with id + keys conflict detection
  • Built-in defaults: search, help, close
  • Common-command catalog (cherry-pick into your app)
  • Input guard with per-shortcut allowInInputs opt-in
  • Built-in actions: click, focus, submit, toggle help
  • Custom handler callback per shortcut
  • Auto-rendered help panel (modal + inline variants)
  • Live re-render of help panel as the registry changes
  • Per-platform key humanization (⌘ on Mac, Ctrl elsewhere)
  • keycuts:invoke CustomEvent for observers
  • Demo with playground + shortcuts list
  • Publish to npm (as @goboldlyforward/keycuts)
  • Deploy demo to gh-pages
  • GitHub Actions CI (eslint + stylelint)
  • Key-sequence support (g then i for Gmail-style nav)
  • Scoped registries (per-page or per-modal shortcut sets)
  • Optional keycuts-rails gem wrapper

License

MIT — see LICENSE.

About

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.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors