@waradu/keyboard

A keyboard manager compatible with JavaScript, TypeScript and Nuxt
5,704 1
Install
npm install @waradu/keyboard
Latest Version:9.3.0
Vue:3
License:MIT
Last Updated:Sep 25, 2026
Links: GitHub  ·  npm
Maintainer: waradu

Keyboard Manager

A keyboard manager compatible with JavaScript, TypeScript and Nuxt.

validate

Install

bun install @waradu/keyboard

Get Started

Create a keyboard instance and bind a shortcut:

import { Keyboard } from "@waradu/keyboard";

const keyboard = new Keyboard();

const off = keyboard.bind({
  keys: "ctrl+k",
  run({ event }) {
    console.log("pressed", event.key);
  },
});

off();

Keyboard initializes itself when window is available. If you create an instance before window exists, call keyboard.init() later.

Nuxt

Add the Nuxt module to nuxt.config.ts:

export default defineNuxtConfig({
  modules: ["@waradu/keyboard/nuxt"],
});

The module provides $keyboard, auto-imports composables, registers directives and initializes the keyboard on app:mounted.

const off = useKeybind({
  keys: "a",
  run() {
    console.log("A key pressed");
  },
});

useKeybind mirrors keyboard.bind and automatically removes the binding when the component unmounts.

useKeybind([
  {
    keys: "ctrl+z",
    run() {
      console.log("undo");
    },
  },
  {
    keys: "ctrl+shift+z",
    run() {
      console.log("redo");
    },
  },
]);

Create a layer in Nuxt:

const editor = useKeybindLayer("editor");

editor.bind({
  keys: "escape",
  run() {
    editor.disable();
  },
});

Inspect active handlers:

const { handlers, unsubscribe } = useKeyboardInspector();

console.log(handlers.value);
unsubscribe();

Record keybinds:

const stop = useKeybindRecorder((keybind) => {
  console.log(keybind.toString());
});

stop();

Access the provided instance directly:

const { $keyboard } = useNuxtApp();

$keyboard.destroy();

The module accepts debug and capture options, both defaulting to false:

export default defineNuxtConfig({
  modules: ["@waradu/keyboard/nuxt"],
  keyboard: {
    debug: false,
    capture: false,
  },
});

Usage

Bind One Handler

const off = keyboard.bind({
  keys: ["ctrl+y", "ctrl+shift+z"],
  run() {
    console.log("redo");
  },
  config: {
    prevent: true,
  },
});

off();

keys can be a key string, a Keybind, a plain keybind shape, or an array of them.

keyboard.bind({
  keys: {
    platform: "macos",
    key: "a",
    modifiers: {
      alt: true,
    },
  },
  run() {},
});

Bind Multiple Handlers

keyboard.bind([
  {
    keys: "ctrl+z",
    run() {
      console.log("undo");
    },
  },
  {
    keys: "ctrl+shift+z",
    run() {
      console.log("redo");
    },
  },
]);

You can pass shared config as the second argument:

keyboard.bind(
  [
    {
      keys: "ctrl+s",
      run() {
        console.log("save");
      },
    },
  ],
  {
    prevent: true,
  },
);

Inspect Handlers

const unsubscribe = keyboard.subscribe((handlers) => {
  console.log("active handlers", handlers);
});

unsubscribe();

Record Keybinds

const stop = keyboard.record((keybind) => {
  console.log(keybind.toString());
});

stop();

Keybinds

Use Keybind when you need to parse, compare, format, or display shortcuts.

import { Keybind } from "@waradu/keyboard";

const keybind = Keybind.fromString("ctrl-cmd+shift+k");

keybind?.toString(); // "ctrl-cmd+shift+k"
keybind?.toShape(); // { key, modifiers, platform? }
keybind?.equals("ctrl-cmd+shift+k"); // true
Keybind.equals("ctrl+k", "ctrl+k"); // true

Readable methods return parts so you can join them into text or render them as separate keycaps.

keybind?.toReadable(); // ["Ctrl-Cmd", "Shift", "K"]
keybind?.toLocalReadable({ platform: "macos" }); // ["Cmd", "Shift", "K"]
keybind?.toLocalReadable({ platform: "windows" }); // ["Ctrl", "Shift", "K"]

Keybind.fromString("no-macos:alt+enter")?.toReadablePlatform(); // "Linux & Windows"

Key Strings

Key strings describe the key and modifiers that must be active for a handler to run.

The structure is:

(platform:)?(meta+)?(ctrl+)?(ctrl-cmd+)?(alt+)?(shift+)?key

Special keys:

  • $any: match any key and ignores modifiers
  • $num: match any physical number-row or numpad key (0–9) and expose its number as context.template

Platform prefixes:

  • macos
  • win
  • linux
  • no-macos
  • no-win
  • no-linux

The modifier order is fixed: meta, ctrl, ctrl-cmd, alt, shift, then the key.

ctrl-cmd maps to Cmd (meta) on macOS and Ctrl (ctrl) on other platforms. It can be combined with alt and shift, but not with meta or ctrl. In KeybindShape / CreateKeybindShape objects, the same modifier field is named ctrlCmd.

Examples:

  • "ctrl+x": valid
  • "ctrl-cmd+k": valid
  • "ctrl-cmd+shift+k": valid
  • "meta+ctrl+alt+shift+arrow-up": valid
  • "c": valid
  • "macos:x": valid
  • "alt+$num": valid
  • "$any": valid
  • "": invalid
  • "shift+alt+y": invalid, because shift comes after alt
  • "meta+ctrl": invalid, because the key is missing
  • "meta+ctrl-cmd+k": invalid, because ctrl-cmd cannot be mixed with meta or ctrl
  • "lunix:x": invalid platform
  • "xy": invalid, because only one key can be used

Handler

The handler receives a context object:

keyboard.bind({
  keys: "enter",
  run(context) {
    context.event;
    context.handler;
    context.template;
  },
});

Context fields:

  • context.event: the original KeyboardEvent
  • context.handler: the registered handler
  • context.template: the matched template value, currently used by $num

Handler return values are ignored.

Layers

Layers group handlers so they can be enabled or disabled together.

const editor = keyboard.layers.create("editor");

editor.bind({
  keys: "escape",
  run() {
    console.log("escape in editor layer");
  },
});

editor.disable();
editor.enable();
editor.toggle();
editor.off();

Create a disabled layer:

const modal = keyboard.layers.create("modal", true);

Manage layers globally:

keyboard.layers.enable("editor");
keyboard.layers.disable(["modal", "editor"]);
keyboard.layers.set("modal");
keyboard.layers.all();
keyboard.layers.none();

You can also assign layers directly through config:

keyboard.bind({
  keys: "escape",
  run() {},
  config: {
    layers: ["modal"],
  },
});

Config

Keyboard-level config:

const keyboard = new Keyboard({
  debug: false,
  capture: false,
  platform: "macos",
  signal: abortController.signal,
});

Handler config:

keyboard.bind({
  keys: "enter",
  run() {},
  config: {
    once: true,
    ignoreIfEditable: true,
    ignoreRepeat: true,
    runIfFocused: [document.getElementById("email")],
    prevent: true,
    stop: true,
    when: true,
    signal: abortController.signal,
  },
});

Available handler config:

  • once: remove the handler after the first run
  • ignoreIfEditable: skip while an editable element is focused
  • ignoreRepeat: skip repeated keydown events
  • runIfFocused: only run if one of the provided elements is focused
  • prevent: call event.preventDefault()
  • stop: call stopPropagation, stopImmediatePropagation, or both
  • when: boolean or predicate receiving the same handler context as run
  • layers: layer names assigned to the handler
  • signal: abort signal that removes the handler

stop accepts:

  • true: call event.stopPropagation()
  • "immediate": call event.stopImmediatePropagation()
  • "both": call both propagation methods

Directives

Directives are Nuxt-only. Pass the handler to v-keybind and use the directive arg as the key sequence.

<input
  type="text"
  v-keybind:enter="
    () => {
      console.log('Hello, directive!');
    }
  "
/>

Put keyboard modifiers in the key sequence. prevent and stop are directive modifiers:

<input type="text" v-keybind:ctrl+shift+enter.prevent.stop="onEnter" />

Use ctrl-cmd in the key sequence for the cross-platform Cmd/Ctrl key:

<input type="text" v-keybind:ctrl-cmd+k="openCommandPalette" />

For platform-aware or multi-key bindings, use useKeybind.

The directive automatically limits the handler to that focused element.

Changes

v8 -> v9

  • Replaced useKeyboard() with the Keyboard class
  • Renamed keyboard.listen to keyboard.bind
  • Renamed the control modifier to ctrl
  • Changed key-string separators from _ to +
  • Changed catch-all key from any to $any
  • Added the Keybind class for parsing, formatting, comparing, and converting keybinds
  • Added readable Keybind labels for rendering shortcuts in UI
  • Added automatic initialization when window is available
  • Added ctrl-cmd as a cross-platform Cmd/Ctrl modifier
  • Added pause(), resume(), and toggle() for temporarily pausing the keyboard
  • Redesigned the Nuxt directive API to use v-keybind:sequence with .prevent and .stop
  • Fixed run return type by ignoring it
  • Better and faster OS detection
  • Removed stats
  • Fixed $num template
  • ignoreRepeat option
  • Send context to when
  • Add metadata to keybinds
  • Add visible on helper
  • Add Nuxt UI kbd compatible keybind converter

v7.4 -> v8

  • Added parseKeyData to parse key data into a key string
  • Renamed FormattedKeySequence to KeyData
  • KeyData can now also be used to define keys
  • Changed KeyData format

v7.3 -> v7.4

  • Added keyboard.exists to check if a key-string handler already exists
  • Added config.when to control whether a handler runs

v7.2 -> v7.3

  • Added keyboard.layers to create and manage layers
  • Added Nuxt-only useKeybindLayer composable

v7.1 -> v7.2

  • Added keyboard.subscribe for inspecting active handlers
  • Added keyboard.record to record a keybind
  • Added parseKeyString to parse a key string into key data
  • Added Nuxt-only useKeyboardInspector and useKeybindRecorder

v7 -> v7.1

  • Added v-keybind and v-run directives
  • Allowed passing a single sequence as the keys argument instead of requiring an array

Development

You need Bun.

bun install

Commands:

  • bun test: run tests
  • bunx tsc --noEmit: run type checking
  • bun run lint: run linting
  • bun run format: format files
  • bun run format:check: check formatting
  • bun playground:prepare: install playground dependencies
  • bun playground: start the playground

Examples

Catch any key press:

keyboard.bind({
  keys: "$any",
  run(ctx) {
    console.log("Key pressed:", ctx.event.key);
  },
});

Run only when an input is focused:

const input = document.getElementById("myInput");

keyboard.bind({
  keys: "enter",
  run() {
    console.log("Enter pressed while input is focused");
  },
  config: {
    runIfFocused: [input],
  },
});

Prevent default behavior:

keyboard.bind({
  keys: "ctrl+r",
  run() {
    console.log("Refresh prevented");
  },
  config: {
    prevent: true,
  },
});

Run a handler only once:

keyboard.bind({
  keys: "escape",
  run() {
    console.log("Escape pressed once");
  },
  config: {
    once: true,
  },
});

Platform-aware undo and redo:

keyboard.bind([
  {
    // With ctrl-cmd
    keys: "ctrl-cmd+z",
    run() {
      console.log("undo");
    },
  },
  {
    // Manually
    keys: ["no-macos:ctrl+shift+z", "macos:meta+shift+z"],
    run() {
      console.log("redo");
    },
  },
]);

Catch Alt plus any number:

keyboard.bind({
  keys: "alt+$num",
  run(ctx) {
    console.log("Number pressed:", ctx.template);
  },
});

Related Packages

@4rk/vue-mousetrap

Mousetrap directive for Vue

2,602 2
@tanstack/vue-hotkeys

Vue adapter for TanStack Hotkeys

113,894 746
vue-shortcuts

Vue plugin for keyboard shortcuts

2,995 7
browser-shortcuts

Declarative keyboard, mouse and wheel shortcuts for React, Preact, Vue, Svelte,...

520 0
v-hotkey

Vue 2.x directive - binding hotkeys for components.

373,650 713

Version History

Version Released Vue License
9.3.0 ^3.0 MIT
9.2.0 ^3.0 MIT
9.1.2 ^3.0 MIT
9.1.1 ^3.0 MIT
9.1.0 ^3.0 MIT
Pre-releases (1)
Version Released License
5.0.0-rc.0 MIT