npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@keybindy/core

v2.0.2

Published

A lightweight and framework-agnostic keyboard shortcut manager for web apps. Define, register, and handle keybindings with ease.

Readme

@keybindy/core


Handling keyboard shortcuts in complex web applications starts simple, but quickly turns chaotic:

  • You open a modal and global shortcuts trigger in the background.
  • You type in a search input and hotkeys fire unintentionally.
  • Conflicting tools (like Canvas vs. Layer panel) fight over the same keys.
  • Modifier keys on Mac vs. Windows require messy conditional code.

Keybindy is built from the ground up to solve this with a clean, centralized, and delightful API. It weighs ~2KB, has zero dependencies, and works anywhere JavaScript runs — Vanilla JS, React, Vue, Svelte, or Solid.


✨ Features

  • ⚡️ Zero Dependencies & Ultra-Lightweight — Tiny footprint (~2KB gzipped).
  • 🎹 3 Shortcut Models — Simultaneous chords (Ctrl+S), Vim-style sequences (G then D), and Hold actions (push-to-talk).
  • 🎯 Smart Scoping & Layering — Seamlessly isolate modals or cascade shortcuts across complex layered tools.
  • ⚖️ Priority Resolution — Layer child tools over parent canvases with numeric priority weights.
  • 🛡 Guard & Interceptor Hooks — Run global or scoped beforeEach and afterEach middlewares.
  • 📝 Smart Input Detection — Automatically ignores typing inside inputs, textareas, and contenteditable elements (with easy per-shortcut overrides).
  • 🍎 Forgiving Cross-Platform Aliases — Write Cmd, Command, Option, Alt, Esc, or Escape — Keybindy normalizes them automatically.
  • 🔒 100% TypeScript — Strict type safety and rich IDE autocomplete.

📦 Installation

# npm
npm install @keybindy/core

# pnpm
pnpm add @keybindy/core

# bun
bun add @keybindy/core

🚀 Quick Start

import ShortcutManager from '@keybindy/core';

// 1. Initialize
const shortcuts = new ShortcutManager({
  ignoreInputs: true, // Automatically ignore hotkeys when typing in inputs/textareas
});

// 2. Register standard shortcuts
shortcuts.register(['Ctrl', 'S'], (event) => {
  console.log('Saved document!');
}, { preventDefault: true });

// 3. Multi-platform bindings (Cmd on Mac, Ctrl on Windows)
shortcuts.register([['Meta', 'K'], ['Ctrl', 'K']], () => {
  console.log('Search palette opened!');
}, { preventDefault: true });

💡 Shortcut Types

1. Simultaneous Chords

Fires when all specified keys are pressed down together:

shortcuts.register(['Ctrl', 'Shift', 'P'], () => {
  openCommandPalette();
}, { preventDefault: true });

2. Sequential Chains (Vim / GitHub style)

Fires when keys are pressed in sequence within a configurable timeout window:

// Press 'G' then 'D' to Go to Dashboard
shortcuts.register(['G', 'D'], () => {
  navigateTo('/dashboard');
}, {
  sequential: true,
  sequenceDelay: 800, // max ms between keystrokes (default: 1000)
});

3. Holdable Actions (Push-to-Talk / Pan Tool)

Fires both on down and up, allowing continuous actions while held:

shortcuts.register(['Space'], (event, state) => {
  if (state === 'down') {
    enableCanvasPanning();
  } else {
    disableCanvasPanning();
  }
}, { hold: true });

🎯 Scopes & Layering (default vs cascade)

Keybindy provides two distinct scoping modes to suit any application UI:

A. default Mode — Strict Isolation (e.g. Modals & Dialogs)

In default mode, only the single topmost active scope is enabled. All other scopes (like global) are completely deactivated while the modal is open.

// 1. Register global shortcut
shortcuts.register(['Delete'], deleteSelectedCard, { scope: 'global' });

// 2. Register modal shortcut
shortcuts.register(['Esc'], closeModal, { scope: 'modal', enableInInput: true });

// 3. Open Modal
shortcuts.setActiveScope('modal');
// -> 'Delete' is now blocked so background elements cannot be deleted!

// 4. Close Modal
shortcuts.popScope('modal');
// -> 'global' is restored automatically!

B. cascade Mode — Layered Tools & Priority (e.g. Figma / Photoshop)

In cascade mode, all active scopes work together. Non-conflicting parent shortcuts pass through, while colliding keys are resolved by numeric priority weights or stack order.

const shortcuts = new ShortcutManager({ scopeMode: 'cascade' });

// Global shortcut
shortcuts.register(['Space'], panCanvas, { scope: 'global' });
shortcuts.register(['V'], selectTool, { scope: 'global' });

// Text Tool Scope (higher priority)
shortcuts.register(['V'], pasteFormattedText, { scope: 'text-editor' });
shortcuts.setScopePriority('text-editor', 100);

// When 'text-editor' is active:
// - 'Space' still pans the canvas (passes through)
// - 'V' triggers pasteFormattedText (overrides global 'V' because 100 > 0)

[!TIP] Isolating a Child / Modal in Cascading Mode: If your app runs in cascade mode, but you want a specific modal or dialog to trap and block all parent hotkeys, switch to 'default' mode while the modal is open:

// Open isolated modal
shortcuts.setScopeMode('default');
shortcuts.setActiveScope('modal');

// Close modal & restore cascade
shortcuts.popScope('modal');
shortcuts.setScopeMode('cascade');

🛡 Guard & Interceptor Hooks (beforeEach / afterEach)

Add global or scoped middlewares before or after shortcuts execute:

// 🛑 Guard: Block all shortcuts when an async modal is loading
const unregisterGuard = shortcuts.beforeEach((shortcut, event) => {
  if (isAppBusy) {
    console.warn('Action blocked: App is busy.');
    return false; // Returning false cancels the shortcut execution
  }
});

// 📊 Interceptor: Log analytics after every shortcut in the 'canvas' scope
shortcuts.afterEach((shortcut, event) => {
  analytics.track('Shortcut Triggered', { keys: shortcut.keys });
}, { scope: 'canvas' });

📝 Input & Editable Target Handling

By default, shortcuts won't trigger while the user is typing inside <input>, <textarea>, <select>, or [contenteditable="true"] elements when ignoreInputs: true is enabled.

You can customize this globally or per shortcut:

// Global setting
const shortcuts = new ShortcutManager({ ignoreInputs: true });

// Override per-shortcut: Allow Escape to close dialogs even while focusing an input
shortcuts.register(['Esc'], closeDialog, {
  enableInInput: true,
});

📖 API Reference

ShortcutManager Options

const manager = new ShortcutManager({
  ignoreInputs?: boolean;             // Default: false
  scopeMode?: 'default' | 'cascade';  // Default: 'default'
  silent?: boolean;                   // Default: true (suppress debug logs)
  onShortcutFired?: (info) => void;   // Event telemetry callback
});

Registration Options (ShortcutOptions)

manager.register(keys, handler, {
  scope?: string;             // Scope identifier (default: 'global')
  preventDefault?: boolean;   // Calls event.preventDefault()
  stopPropagation?: boolean;  // Calls event.stopPropagation()
  sequential?: boolean;       // Treat as sequential chain (e.g. G then D)
  sequenceDelay?: number;     // Milliseconds allowed between sequential keys
  hold?: boolean;             // Fires with 'down' and 'up' state
  repeat?: boolean;           // Allow continuous firing when holding key down
  enableInInput?: boolean;    // Allow shortcut while typing in inputs
  ignoreInputs?: boolean;     // Explicitly ignore shortcut in inputs
  data?: Record<string, any>; // Custom metadata (labels, descriptions for cheat sheets)
});

Scope & Lifecycle Methods

| Method | Description | | :--- | :--- | | setActiveScope(scope) | Sets the current active scope. | | pushScope(scope) | Pushes a scope onto the stack. | | popScope(scope?) | Pops the topmost scope or removes a specific target scope. | | setScopeMode('default' \| 'cascade') | Switches between strict isolation and layered cascading. | | setScopePriority(scope, priority) | Sets a numeric weight (e.g. 100) for cascade mode. | | removeScopePriority(scope) | Clears a scope's custom priority weight. | | beforeEach(hook, options?) | Registers a guard callback. Return false to abort. | | afterEach(hook, options?) | Registers an interceptor callback after execution. | | enableAll(scope?) / disableAll(scope?) | Enables or disables shortcuts globally or by scope. | | getCheatSheet(scope?) | Returns all registered shortcuts and custom data metadata. | | destroy() | Cleans up all listeners and memory. |


📄 License

MIT © Keybindy Contributors