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

@farskid/kilid

v0.2.0

Published

Fast, zero-dependency TypeScript keyboard, mouse and pointer management with a Monaco-style keybinding API

Downloads

190

Readme

kilid

CI

Keyboard, mouse & pointer bindings without the weight.

A zero-dependency TypeScript library for DOM keyboard, mouse and pointer event bindings — packed into a couple of kilobytes with zero-allocation dispatch.

Docs & live demo →

| | | |---|---| | Core (no chords) | 1.6 KB gzip | | With chords | 1.8 KB gzip | | Dependencies | 0 | | Dispatch throughput | ~2.5M/sec |

npm install @farskid/kilid

Why kilid

  • Compact numeric encoding — bindings are single numbers: KeyMod.CtrlCmd | KeyCode.KeyS, chords via KeyChord(...) — one integer per binding, resolved at registration.
  • Zero-allocation dispatch — every event reduces to one integer hash and one Map lookup. No strings, objects or closures on the hot path — flat cost at any binding count.
  • Pay only for what you use — chords, string parsing, the pointer service and the React adapter are separate modules. A Cmd+S-only app ships 1.6 KB.
  • Cross-platform by defaultKeyMod.CtrlCmd means Cmd on macOS and Ctrl elsewhere. One binding, correct everywhere, resolved once at registration.
  • Layout-independent — bindings match physical keys via KeyboardEvent.code, with an event.key fallback for exotic keyboards.
  • One pointing surface — pointer events subsume mouse. A single service covers down/move/click/wheel with the same modifier encoding, plus pen/touch filters.

How kilid compares

| Library | Keyboard | Chords | Pointer / mouse | Zero deps | Tree-shakeable | Framework hooks | Typical core gzip | |---|---|:---:|:---:|:---:|:---:|---|---:| | kilid | ✓ | ✓ opt-in | ✓ | ✓ | ✓ | React | 1.6 KB | | Mousetrap | ✓ | ✓ | — | ✓ | — | — | ~2 KB | | hotkeys-js | ✓ | — | — | ✓ | partial | — | ~3 KB | | tinykeys | ✓ | ✓ | — | ✓ | — | — | ~0.7 KB | | react-hotkeys-hook | ✓ | ✓ | — | — | partial | React | ~4 KB+ | | @tanstack/react-hotkeys | ✓ | ✓ | — | — | partial | React | ~5 KB+ | | cmdk (palette UI) | partial | — | — | — | — | React | ~8 KB+ |

Competitor sizes are approximate (typical single-import usage). kilid numbers are from CI size scenarios.

When kilid fits: editors, canvases, dashboards — keyboard + pointer + chords in one package. When something else fits: keyboard-only React hotkeys (tinykeys, react-hotkeys-hook); command palette UI (cmdk — pair with kilid, see Recipes).

Quick start

import { KeyMod, KeyCode, keybindings } from '@farskid/kilid';

const keys = keybindings(window);

// Cmd+S on macOS, Ctrl+S elsewhere — a single-part binding
keys.add(KeyMod.CtrlCmd | KeyCode.KeyS, (e) => save());

// Guards and event control
const off = keys.add(KeyMod.CtrlCmd | KeyCode.KeyP, quickOpen, {
  when: () => !modalIsOpen,  // evaluated at dispatch
  preventDefault: true,       // default true for keyboard
});

off();          // add() returns an unsubscribe function
keys.dispose(); // removes all bindings and the DOM listener

API reference

Everything ships from two entry points: @farskid/kilid (core) and @farskid/kilid/react (adapter). All factories return plain objects; all add() calls return an unsubscribe function. Invalid encodings register nothing — the core never throws; in development builds a console.warn explains why (stripped from production bundles via process.env.NODE_ENV).

keybindings(target, options?)

Single-part keybinding dispatcher (Cmd+S, F2, Ctrl+Shift+P) using one keydown listener. Options: isMac (override platform detection), capture.

const keys = keybindings(element, { isMac: false });
const off = keys.add(encoded, handler, { when, preventDefault, stopPropagation });
keys.dispose();

chordKeybindings(target, options?)

Drop-in superset of keybindings that also handles two-part chords (Ctrl+K Ctrl+S) with prefix-then-second-key semantics: a chord prefix shadows single bindings on the same combo, an unmatched second key is swallowed, and the pending prefix expires after chordTimeout (default 5000 ms). Separate module — apps without chords never ship the state machine.

Note: Cmd+S is a single binding with a modifier, not a chord; chords are two sequential keypresses.

import { KeyChord, chordKeybindings } from '@farskid/kilid';

const keys = chordKeybindings(window);
keys.add(
  KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyS),
  openKeyboardShortcuts
);
keys.isChordPending; // true between Ctrl+K and the second part

pointerBindings(target, options?)

One service for the whole pointing surface. Event kinds: down, up, move, enter, leave, cancel, click, dblclick, contextmenu, wheel. DOM listeners attach lazily per kind and detach when the last binding of that kind unsubscribes. preventDefault defaults to false here.

import { KeyMod, MouseButton, pointerBindings } from '@farskid/kilid';

const pointer = pointerBindings(element);

pointer.add(KeyMod.CtrlCmd | MouseButton.Left, 'click', addToSelection);
pointer.add(MouseButton.Middle, 'down', startPan);
pointer.add(KeyMod.CtrlCmd | MouseButton.WheelUp, 'wheel', zoomIn, { preventDefault: true });

// Buttonless kinds (move/enter/leave/cancel) take no button — with pen/touch filters
pointer.add('move', onDraw, { pointerType: ['pen', 'touch'] });

// Modifier-only encoding: move while Alt (or Cmd+Alt) is held
pointer.add(KeyMod.Alt, 'move', onAltDraw);
pointer.add(KeyMod.CtrlCmd | KeyMod.Alt, 'move', onOrbit);

For move/enter/leave/cancel (where button is -1), use the buttonless overload add('move', handler) or a modifier-only encoding — button bits register nothing there (dev builds warn). Pointer events don't carry non-modifier key state, so "move while K is held" needs a when guard fed by your own keydown/keyup tracking. Use when with event.buttons to filter by held buttons.

Encoding: KeyMod, KeyCode, MouseButton, KeyChord

A binding is one 32-bit number with a fixed bit layout:

15 14 13 12 11 10  9  8  7 ... 0
 -  -  C  S  A  W  [ key code ]     C = CtrlCmd  S = Shift  A = Alt  W = WinCtrl

KeyChord(first, second)  // packs the second part into bits 16–31

| Export | Meaning | |---|---| | KeyMod.CtrlCmd | Cmd on macOS, Ctrl on Windows/Linux | | KeyMod.WinCtrl | Ctrl on macOS, Win/Meta on Windows/Linux | | KeyMod.Shift, KeyMod.Alt | Shift; Alt (Option on macOS) | | KeyCode.* | Layout-independent key codes — KeyA–Z, Digit0–9, F1–19, Numpad0–9, Enter, Escape, arrows, punctuation, … (names match KeyboardEvent.code) | | MouseButton.* | Left, Middle, Right, X1, X2, WheelUp/Down/Left/Right | | keyCodeFromEvent(e) | Resolve a live KeyboardEvent to a key code | | isModifierKeyCode(c) | True for Shift/Ctrl/Alt/Meta | | decodeKeybinding(n, isMac) | Encoded binding → platform-resolved parts |

Strings: parseKeybinding / formatKeybinding

String convenience lives in its own module with lazily built tables, so it only ships to bundles that import it. The core add() is numeric-only by design — the parser is never pulled in behind your back.

import { parseKeybinding, formatKeybinding } from '@farskid/kilid';

keys.add(parseKeybinding('Ctrl+Shift+P'), quickOpen);
keys.add(parseKeybinding('Ctrl+K Ctrl+S'), openShortcuts); // chordKeybindings only

formatKeybinding(KeyMod.CtrlCmd | KeyCode.KeyS);                  // "Ctrl+S"
formatKeybinding(KeyMod.CtrlCmd | KeyCode.KeyS, { isMac: true }); // "⌘S"

In strings, Ctrl, Cmd, Meta and Mod all map to KeyMod.CtrlCmd so one string works on every platform; use WinCtrl/Super for the secondary modifier. Also exported: keyCodeToString, keyCodeFromString (accepts aliases like Esc).

Recipes

Common patterns that aren't separate API options — capture is a factory flag, one-shot bindings are a few lines of glue, and delegation is native DOM bubbling on whatever EventTarget you pass.

Capture phase

Pass capture: true when creating the service. It applies to the whole dispatcher's DOM listener (all bindings on that instance), not per-binding. keybindings, chordKeybindings, and pointerBindings all accept it. React hooks pass it through as capture (service-level — hooks on the same target share a listener only when capture matches).

import { KeyMod, KeyCode, MouseButton, keybindings, pointerBindings } from '@farskid/kilid';

// Intercept before targets deeper in the tree (e.g. stop browser shortcuts early)
const keys = keybindings(document, { capture: true });
keys.add(KeyMod.CtrlCmd | KeyCode.KeyS, save);

const list = document.getElementById('list');
const pointer = pointerBindings(list, { capture: true });
pointer.add(MouseButton.Left, 'click', onRowClick);

One-shot binding

There is no { once: true } option — call the unsubscribe function returned by add() inside the handler when you only want one fire.

const keys = keybindings(window);

const off = keys.add(KeyMod.CtrlCmd | KeyCode.KeyS, (e) => {
  off(); // unregister before running — safe even if save throws
  save(e);
});

// same idea for pointer
const pointer = pointerBindings(element);
const offClick = pointer.add(MouseButton.Left, 'click', (e) => {
  offClick();
  confirmOnce(e);
});

Event delegation (bubbling)

Attach to a parent; bubbling events from children reach the listener. There is no built-in selector API — filter with event.target and Element.closest() inside the handler. The when guard receives no event, so it can't filter by target; use it for app-state conditions (() => !modalIsOpen) instead. pointerenter/pointerleave do not bubble; bind those on the element you care about. keydown bubbles too, but its target is whatever element has focus — so keyboard bindings usually go on window rather than a container.

const list = document.getElementById('list');
const pointer = pointerBindings(list);

pointer.add(MouseButton.Left, 'click', (e) => {
  const row = e.target.closest('[data-id]');
  if (!row) return;
  select(row.dataset.id);
}, {
  when: () => !modalIsOpen, // app-state guard, checked before the handler runs
});

Testing (Vitest / Playwright)

Import @farskid/kilid/testing to dispatch DOM events that match the same numeric encodings your app registers:

import { KeyMod, KeyCode, MouseButton, keybindings } from '@farskid/kilid';
import { dispatchKeybinding, dispatchPointerBinding } from '@farskid/kilid/testing';

const keys = keybindings(window);
keys.add(KeyMod.CtrlCmd | KeyCode.KeyS, save);

dispatchKeybinding(window, KeyMod.CtrlCmd | KeyCode.KeyS);
expect(save).toHaveBeenCalled();

dispatchPointerBinding(canvas, MouseButton.Left, 'down');

Also exported: dispatchKeyPart, dispatchKeybindingString, keyCodeToDomCode.

Command palette (cmdk / kbar)

cmdk and kbar render the palette UI — kilid handles global shortcuts to open/close it and app-wide bindings (⌘S, etc.) with when guards:

import { KeyMod, KeyCode, keybindings } from '@farskid/kilid';

const keys = keybindings(document, { capture: true });
let paletteOpen = false;

keys.add(KeyMod.CtrlCmd | KeyCode.KeyK, () => {
  paletteOpen = true;
  setOpen(true);
}, { when: () => !paletteOpen, preventDefault: true });

keys.add(KeyCode.Escape, () => {
  paletteOpen = false;
  setOpen(false);
}, { when: () => paletteOpen });

In React, use useKeybinding(KeyMod.CtrlCmd | KeyCode.KeyK, open, { when: () => !open }) in your root layout. cmdk/kbar own arrow-key navigation inside the open palette.

Radix UI / shadcn (dialogs, menus, command)

Radix primitives (shadcn/ui) manage focus traps inside overlays. kilid handles global shortcuts with when guards tied to your open state:

import { useState } from 'react';
import { KeyCode } from '@farskid/kilid';
import { useKeybinding, useParsedKeybinding } from '@farskid/kilid/react';

function AppShell() {
  const [commandOpen, setCommandOpen] = useState(false);

  useParsedKeybinding('Ctrl+K', () => setCommandOpen(true), {
    when: () => !commandOpen,
    capture: true,
  });
  useKeybinding(KeyCode.Escape, () => setCommandOpen(false), {
    enabled: commandOpen,
  });

  return <CommandDialog open={commandOpen} onOpenChange={setCommandOpen} />;
}

Gate destructive globals (⌘S, delete) while Dialog / AlertDialog is open. shadcn Command + cmdk owns in-palette navigation; kilid opens it.

React adapter

@farskid/kilid/react is a separate build entry with react as an optional peer dependency — if you never import it, no React-related code enters your bundle. Hooks are split for tree-shaking: useKeybinding (singles only), useChordKeybinding (chords), useParsedKeybinding (strings + parser), and usePointerBinding.

import { KeyMod, KeyCode, KeyChord, MouseButton } from '@farskid/kilid';
import {
  useKeybinding,
  useChordKeybinding,
  useParsedKeybinding,
  usePointerBinding,
} from '@farskid/kilid/react';

function Editor() {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useKeybinding(KeyMod.CtrlCmd | KeyCode.KeyS, save);   // lean — no chord machinery
  useChordKeybinding(
    KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyS),
    openShortcuts
  );
  useParsedKeybinding('Ctrl+Shift+P', quickOpen);       // pulls in parseKeybinding

  usePointerBinding(KeyMod.CtrlCmd | MouseButton.Left, 'click', addToSelection, {
    target: canvasRef,
  });

  return <canvas ref={canvasRef} />;
}

Hook options mirror the core API: target (EventTarget or ref, default window), when, enabled, preventDefault, stopPropagation, capture, isMac, chordTimeout (keyboard), and pointerType (pointer hook only). Option parity is enforced by test/adapter-contract.test.ts so future framework adapters stay aligned with the core.

  • Latest-ref handlers — inline closures are fine; changing the handler or guard re-registers nothing and costs zero per-render work.
  • Structural deps only — bindings re-register only when the encoding, target, kind or flags actually change; inline pointerType arrays cause no churn.
  • Refcounted service sharing — hooks on the same target and the same service options (capture, isMac, …) share one listener; the last unmount disposes it.

Vue — @farskid/kilid/vue

Composables: useKeybinding, useChordKeybinding, useParsedKeybinding, usePointerBinding. Accept Vue refs for target via { value }.

Solid — @farskid/kilid/solid

createKeybinding, createChordKeybinding, createParsedKeybinding, createPointerBinding — pass accessor functions for handlers.

Svelte — @farskid/kilid/svelte

bindKeybinding, bindChordKeybinding, bindParsedKeybinding, bindPointerBinding — return cleanup; wrap in $effect.

Angular — @farskid/kilid/angular

bindKeybinding / bindPointerBinding for use in afterNextRender or services. Standalone attribute directives are in src/angular/directives.ts (copy into your app).

Performance & size

The hot path for every event: bitwise hash (modifiers + code packed into one int) → one integer-keyed Map.get() → handler call. Zero allocations, matched or not. Dispatch cost is flat with respect to the number of registered bindings (~2.5M dispatches/sec in benchmarks, 10 vs 500 bindings within 10%).

| Bundle scenario | Minified | Gzipped | |---|---:|---:| | keybindings only (no chords) | 3.3 KB | 1.6 KB | | chordKeybindings | 3.7 KB | 1.8 KB | | Keyboard + pointer | 5.1 KB | 2.3 KB | | Everything incl. parse/format | 7.8 KB | 3.3 KB | | React: useKeybinding only | 4.5 KB | 2.1 KB | | React: useKeybinding + usePointerBinding | 7.0 KB | 2.9 KB | | React: all hooks | 9.1 KB | 3.7 KB |

Sizes are enforced in CI with per-scenario budgets and reported as a comment on every pull request. Size-oriented design: factories instead of classes (state minifies to single-letter closure variables), the KeyCode table generated at runtime from packed strings (typed statically via a template-literal union), no reverse enum mappings, no defensive throws, and every convenience layer in its own tree-shakeable module.

Run npm run bench for numbers.

Development

npm install
npm test              # unit tests (vitest + happy-dom)
npm run test:browser  # smoke tests (Playwright + Chromium)
npm run bench         # dispatch benchmarks
npm run size          # bundle-size scenarios → scripts/size-scenarios/.out/
npm run build         # tsup -> dist (esm + cjs + d.ts)

The landing page lives in docs/ and deploys to GitHub Pages on every push to main that touches docs/**.

License

MIT