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

mokeys

v0.6.0

Published

> [!WARNING] > Pre 1.0. The API *will change*, without deprecation warnings. Things are still > in flux while I am using this in sideprojects.

Downloads

177

Readme

mokeys

[!WARNING] Pre 1.0. The API will change, without deprecation warnings. Things are still in flux while I am using this in sideprojects.

A small library for managing modal keyboard shortcuts for web frontends.

Installation

npm install mokeys
# or
pnpm add mokeys
# or
yarn add mokeys

Features

  • Zero dependencies
  • Modal shortcuts - different keymaps based on mode (e.g., normal, visual, insert)
  • Key sequences - multi-key bindings like "g g" or "ctrl+w q"
  • Keymap management - add, remove, list, and override bindings dynamically
  • Small - ~8.8kb uncompressed, <3kb gzipped
  • Cross-platform modifier keys
    • super: cmd on macOS, ctrl on Windows/Linux
    • hyper: all four modifiers (ctrl+shift+alt+cmd)
    • meh: ctrl+shift+alt
  • Finite Statemachine - Mokeys is a FSM tailored towards keyboard input and dispatching actions and calling side-effects.
  • swap out side-effects - re-use the same keybinds with different side-effects
  • Fully typed - autocompletion for mode names

Quick Start

import { Keymap } from 'mokeys'

let index = 0
const items = ['a', 'b', 'c']

// Bind to a specific element
const editor = document.querySelector('#editor')
const keymap = new Keymap(editor, {
  initial: 'normal',
  modes: {
    normal: {
      'j': () => {
        if (index < items.length - 1) index++
        console.log('Current:', items[index])
      },
      'k': () => {
        if (index > 0) index--
        console.log('Current:', items[index])
      },
    }
  }
})

// Or bind to window (global)
const globalKeymap = new Keymap({
  initial: 'normal',
  modes: {
    normal: {
      'ctrl+s': () => save()
    }
  }
})

Usage

Constructor Signatures

Mokeys supports two constructor signatures:

// Bind to a specific element
const editor = document.querySelector('#editor')
const keymap = new Keymap(editor, {
  initial: 'normal',
  modes: { /* ... */ }
})

// Bind to window (default)
const keymap = new Keymap({
  initial: 'normal',
  modes: { /* ... */ }
})

// Or specify target in options
const keymap = new Keymap({
  target: document.querySelector('#editor'),
  initial: 'normal',
  modes: { /* ... */ }
})

Note: When using the (element, options) signature, the element parameter takes precedence over the target option.

Effects

Effects are a named collection of functions passed to the Keymap constructor. They get injected into every action's context, accessible via ctx.effects:

const keymap = new Keymap({
  initial: 'normal',
  effects: {
    nextItem: () => { if (currentIndex < items.length - 1) currentIndex++ },
    prevItem: () => { if (currentIndex > 0) currentIndex-- },
    render: () => updateUI()
  },
  modes: {
    normal: {
      'j': ({ effects }) => {
        effects.nextItem()
        effects.render()
      }
    }
  }
})

Why use effects instead of direct function calls?

  1. Decoupling - The keymap becomes a pure description of what should happen, not how. You can define your keymap once and swap the effects implementation:

    const keybinds = { 'a': ({ effects }) => effects.save() }
    
    // Production
    new Keymap({ effects: { save: realSave }, modes: { normal: keybinds } })
    
    // Tests
    new Keymap({ effects: { save: mockSave }, modes: { normal: keybinds } })
  2. Testability - Mock the entire effects object without module import hacks or closures.

  3. Reusability - The same keymap config can be reused across different contexts with different effect implementations.

  4. Type safety - Effects give you a typed contract; the keymap knows what effects are available at compile time.

When to skip effects: For simple apps or tightly coupled logic, direct function calls via closure are simpler:

'j': () => { moveDown(); }

Effects shine when you need dependency injection or want to test/reuse keymaps in isolation from their side effects.

Swapping effects at runtime:

Use setEffects() to change effects without recreating the keymap - useful in SPAs when navigating between pages:

const keymap = new Keymap({
  effects: { save: saveDocument, refresh: loadDocument },
  modes: { normal: { 's': ({ effects }) => effects.save() } }
})

// On route change 
keymap.setEffects({ save: saveUser, refresh: refreshUser })

// Only updates effects given:
keymap.setEffects({ save: saveWhatever })  // refresh is still loadDocument

// Reset all effects (removes effects not in the map)
keymap.setEffects({ save: saveWhatever }, true)  // refresh is now undefined

Inline Functions

You can also use inline functions without defining effects:

const keymap = new Keymap({
  initial: 'normal',
  modes: {
    normal: {
      'j': () => {
        if (currentIndex < items.length - 1) currentIndex++
        updateUI()
      },
      'k': () => {
        if (currentIndex > 0) currentIndex--
        updateUI()
      }
    }
  }
})

Binding Metadata

Add descriptions and groups to bindings for generating help UIs and documentation:

const keymap = new Keymap({
  initial: 'normal',
  effects: {
    moveDown: () => { /* ... */ },
    moveUp: () => { /* ... */ },
    deleteItem: () => { /* ... */ }
  },
  modes: {
    normal: {
      // Plain function (no metadata)
      'x': () => deleteChar(),

      // With description and group
      'j': {
        description: 'Move to next item',
        group: 'Navigation',
        action: () => moveDown()
      },

      // String action - references an effect by name
      'k': {
        description: 'Move to previous item',
        group: 'Navigation',
        action: 'moveUp'  // Calls effects.moveUp()
      },

      'd d': {
        description: 'Delete current item',
        group: 'Editing',
        action: 'deleteItem'
      }
    }
  }
})

// List bindings with metadata (flat)
const bindings = keymap.listCurrent()
// {
//   'j': { action: Function, description: 'Move to next item', group: 'Navigation' },
//   'x': { action: Function, description: undefined, group: undefined }
// }

// List bindings grouped by category
const grouped = keymap.listCurrent(true)
// {
//   'Navigation': [
//     { key: 'j', description: 'Move to next item', action: Function },
//     { key: 'k', description: 'Move to previous item', action: Function }
//   ],
//   'Editing': [
//     { key: 'd d', description: 'Delete current item', action: Function }
//   ],
//   '_ungrouped': [
//     { key: 'x', description: undefined, action: Function }
//   ]
// }

// Customize ungrouped key name
const grouped2 = keymap.listCurrent(true, { ungroupedKey: 'Other' })

String actions: When action is a string, it references an effect by name. This keeps your keymap configuration declarative and makes it easy to swap implementations via setEffects().

Building a Help UI:

function showHelp(keymap: Keymap) {
  const groups = keymap.listCurrent(true)

  for (const [groupName, bindings] of Object.entries(groups)) {
    if (groupName === '_ungrouped') continue

    console.log(`\n## ${groupName}`)
    bindings.forEach(({key, description}) => {
      console.log(`  ${key.padEnd(15)} ${description || ''}`)
    })
  }
}

Modal Keymaps

Create different keymaps for different modes and transition between them:

const keymap = new Keymap({
  initial: 'normal',
  modes: {
    normal: {
      'v': ({keymap}) => keymap.setMode('visual'),
      'i': ({keymap}) => keymap.setMode('insert'),
      'j': () => moveDown(),
      'k': () => moveUp()
    },
    visual: {
      'j': () => { 
        moveDown()
        extendSelection()
      },
      'escape': ({keymap}) => keymap.setMode('normal')
    },
    insert: {
      'escape': ({keymap}) => keymap.setMode('normal')
    }
  }
})

// Programmatic mode transitions
keymap.setMode('visual')

Keymap Management

Dynamically add, remove, override, and list keybindings:

// Add bindings
keymap.add('normal', 'x', () => deleteChar())

// Add bindings with metadata
keymap.add('normal', 'y', {
  description: 'Yank (copy)',
  group: 'Editing',
  action: () => yank()
})

// Override existing bindings (last added wins)
keymap.add('normal', 'j', () => customNext())

// Remove bindings
keymap.remove('normal', 'j')

// List current mode's bindings (flat)
const bindings = keymap.listCurrent()

// List current mode's bindings (grouped)
const grouped = keymap.listCurrent(true)

// List specific mode's bindings (even if not current mode)
const normalBindings = keymap.list('normal')
const normalGrouped = keymap.list('normal', true)

// Add global bindings (work in all modes)
keymap.add({ 'ctrl+s': () => save() })

Mode Transition Events

Hook into mode transitions with onEnter, onLeave, and onChange handlers. All handlers receive a transition object with from, to, and stop properties, plus the keymap instance.

Colocated handlers (defined in mode config):

const keymap = new Keymap({
  initial: 'normal',
  modes: {
    normal: {
      'i': ({keymap}) => keymap.setMode('insert'),
      onLeave: (t, keymap) => {
        if (hasUnsavedChanges) {
          return t.stop  // Cancel the transition
        }
        console.log(`Leaving normal for ${t.to}`)
      }
    },
    insert: {
      'escape': ({keymap}) => keymap.setMode('normal'),
      onEnter: (t, keymap) => {
        if (!canEdit) {
          return t.stop  // Cancel the transition
        }
        console.log(`Entered insert from ${t.from}`)
        showCursor()
      }
    }
  },
  // Global onChange fires for every transition
  onChange: (t, keymap) => {
    console.log(`Mode changed: ${t.from} → ${t.to}`)
  }
})

Dynamic handlers (added after construction):

// Global handlers (fire for any mode)
keymap.onEnter((t, keymap) => console.log(`Entered ${t.to}`))
keymap.onLeave((t, keymap) => console.log(`Left ${t.from}`))
keymap.onChange((t, keymap) => console.log(`Changed to ${t.to}`))

// Mode-specific handlers
keymap.onEnter('insert', (t, keymap) => showCursor())
keymap.onLeave('insert', (t, keymap) => hideCursor())

// Remove handlers
keymap.removeEnter(handler)
keymap.removeEnter('insert', handler)
keymap.removeLeave(handler)
keymap.removeLeave('insert', handler)
keymap.removeChange(handler)

Handler execution order: onLeaveonEnteronChange

Any handler returning t.stop cancels the transition immediately, and subsequent handlers are not called.

Cross-Platform Modifiers

Use platform-aware modifier keys:

keymap.add('normal', {
  'super+s': save,      // cmd+s on macOS, ctrl+s elsewhere
  'hyper+q': quit,      // ctrl+shift+alt+cmd+q (all modifiers)
  'meh+r': reload       // ctrl+shift+alt+r (all except cmd)
})

Key Sequences

Bind multi-key sequences:

keymap.add('normal', {
  'g g': goToTop,           // Press 'g' twice
  'g t': goToTab,           // Press 'g' then 't'
  'ctrl+w q': closeWindow   // Compound sequence
})

Kitchen Sink

All features in a single example:

import { Keymap } from 'mokeys'

let index = 0
const items = ['a', 'b', 'c']
const selected = new Set()

const keymap = new Keymap({
  initial: 'normal',
  effects: {
    nextItem: () => {
      if (index < items.length - 1) index++
    },
    prevItem: () => {
      if (index > 0) index--
    },
    firstItem: () => {
      index = 0
    },
    deleteItem: () => {
      items.splice(index, 1)
    },
    markItem: () => {
      selected.add(index)
    },
    deleteSelection: () => {
      [...selected].sort().reverse().forEach(i => items.splice(i, 1))
      selected.clear()
    },
    render: () => updateUI(),
    save: () => saveToServer()
  },
  global: {
    'ctrl+s': ({effects}) => effects.save()
  },
  modes: {
    normal: {
      'j': ({effects}) => {
        effects.nextItem()
        effects.render()
      },
      'k': ({effects}) => {
        effects.prevItem()
        effects.render()
      },
      'd d': ({effects}) => {
        effects.deleteItem()
        effects.render()
      },
      'g g': ({effects}) => {
        index === items.length - 1 ? effects.firstItem() : effects.nextItem()
        effects.render()
      },
      'v': ({keymap}) => keymap.setMode('visual')
    },
    visual: {
      'j': ({effects}) => {
        effects.nextItem()
        effects.markItem()
        effects.render()
      },
      'k': ({effects}) => {
        effects.markItem()
        effects.prevItem()
        effects.render()
      },
      'd': ({keymap, effects}) => {
        effects.deleteSelection()
        keymap.setMode('normal')
        effects.render()
      },
      'escape': ({keymap}) => keymap.setMode('normal')
    }
  }
})

// Add custom bindings dynamically
keymap.add('normal', 'super+n', () => createNew())

// Listen to mode transitions
keymap.onEnter('visual', () => console.log('Visual mode active'))