keyman Svelte Themes

Keyman

svelte keyboard binding + menu manager

keyman

Headless Svelte 5 library for managing all keyboard input in a web app. One dispatcher, declarative commands, a trie of bindings, reactive state. UI is yours to build.

Status: v0.1 in development. Public API stabilising; not yet on npm. See DESIGN.md for the locked spec.

Why it exists

Single-key hotkeys, VS Code-style chord sequences (Ctrl+K Ctrl+S), and vim-style leader menus (<leader> b r) are usually three different patterns served by three different libs. In keyman they're all paths through one trie — depth is the only thing that changes. Hotkeys are depth-1 paths; leader sequences are deeper paths.

keyman ships zero UI. It owns the document keydown listener, the command registry, the trie, and the reactive state. UI components read that state and render whatever shape they want.

Quick start

import { createKeyman } from 'keyman';

// 1. App context — anything reactive that `when` predicates might read.
const ctx = $state({ modalOpen: false /* ... */ });

// 2. Create an engine. Safe at module load (no DOM access yet).
export const keyman = createKeyman({
    leader: '<Space>',
    context: () => ctx
});

// 3. Register the keymap. Order-independent; validated on start().
keyman.registerCommands([
    { id: 'file.save', label: 'Save File', run: () => /* ... */ }
]);
keyman.registerBindings([
    { keys: '<leader> f s', commandId: 'file.save' },
    { keys: '$mod+s', commandId: 'file.save', allowBrowserShadow: true }
]);

// 4. Attach the document keydown listener after mount.
//    In SvelteKit: usually in +layout.svelte's onMount.
keyman.start();

Consuming reactive state

keyman.currentSequence  // string[] — keys pressed so far in the current chain
keyman.isInMenu         // boolean — true between leader-press and dismiss
keyman.pendingError     // { key } | null — last unmapped key, "stuck" state
keyman.lastFired        // { commandId, timestamp, args } | null — for toasts
keyman.lastError        // { kind, message, timestamp } | null

Read these inside $effect / $derived in your UI components. Don't destructure — capturing values breaks reactivity across the export boundary. See docs/m5-engine-tutorial.md for why.

Vocabulary (locked)

  • Command — what the app can do. Has id, label, run.
  • Binding — a key path bound to a commandId. Many bindings can fire one command.
  • Prefix — display metadata for an intermediate trie node (a submenu).
  • Leader — the configured menu-opener key.
  • Scope — stack entry with bindings; modal UIs push exclusive scopes.
  • Context — reactive app state that when predicates read.

Synonyms (shortcut, hotkey, group, category, handler) are bugs.

Deeper docs

Development

npm run dev          # playground at localhost (src/routes/)
npm run check        # svelte-check
npm run test:unit    # vitest

Repo layout per DESIGN.md §12.1 — flat files in src/lib/, co-located *.test.ts.

Top categories

Loading Svelte Themes