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

@hotora/inputs

v1.0.3

Published

Lightweight JavaScript and TypeScript library for handling keyboard shortcuts, gestures and complex sequences with customizable scoped actions.

Downloads

85

Readme

Inputs

Lightweight JavaScript and TypeScript library for handling keyboard shortcuts, gestures and complex sequences with customizable scoped actions.

Features

  • Step combinations and sequences
  • Scoped handlers (local + global)
  • Element binding
  • Visibility-aware
  • Smart active element resolution
  • Scope propagation control (stopPropagation)
  • Pluggable event providers (DOM, SSR-safe, custom)

Installation

npm install @hotora/inputs

Basic Usage

Default (browser)

import { createInputsManager, Keys } from "@hotora/inputs";

const inputsManager = createInputsManager();

inputsManager.register([Keys.A], {
  handler: () => {
    console.log("Pressed A");
  },
});

For correct operation, it is best to use 1 InputsManager instance at a time. Otherwise stopPropagation may not work.


Step Combinations

Register multi-step combinations:

inputsManager.register([Keys.ControlLeft, Keys.A], {
  handler: () => {
    console.log("Ctrl + A");
  },
});

The handler fires when all steps are fired simultaneously.


Step Sequences

Register ordered sequences:

inputsManager.register([[Keys.ControlLeft], [Keys.A]], {
  handler: () => {
    console.log("Ctrl → A");
  },
});

The handler fires only when steps are fired in order.


Scoped Sequences

Bind sequence to an element and scope:

const element = document.getElementById("editor");

inputsManager.register(
  [Keys.S],
  {
    handler: () => console.log("Save inside editor"),
  },
  element,
  "editor",
);

How scopes work

  1. Scope is attached to an element
  2. On step fire:
  • active element is resolved
  • scope chain is built (element → parents → $global)
  • handlers execute in order

Global Scope

All handlers fallback to $global:

inputsManager.register([Keys.Escape], {
  handler: () => console.log("Global escape"),
});

stopPropagation

Prevent execution in parent scopes:

inputsManager.register([Keys.S], {
  handler: (e) => {
    e.stopPropagation();
    console.log("Handled locally only");
  },
});

Active Element Resolution

InputsManager determines active element using:

  1. Last pointer interaction (click/touch)
  2. Only visible elements
  3. If multiple:
  • prefers last active
  • otherwise deepest

Providers

InputsManager is provider-based.
Event handling is abstracted via EventProvider.

DOMKeyboardEventProvider (used by default)

Uses real DOM APIs:

  • keydown / keyup
  • mousedown / touchstart
  • IntersectionObserver
import { createInputsManager, DOMKeyboardEventProvider } from "@hotora/inputs";

const inputsManager = createInputsManager(new DOMKeyboardEventProvider());

LazyEventProvider (SSR / async)

Safe for SSR and dynamic environments.

  • Buffers subscriptions
  • Initializes provider lazily
  • Supports async factory
import {
  LazyEventProvider,
  DOMKeyboardEventProvider,
  createInputsManager,
} from "@hotora/inputs";

const provider = new LazyEventProvider(() => {
  if (typeof window !== "undefined") {
    return new DOMKeyboardEventProvider();
  }
  return null;
});

const inputsManager = createInputsManager(provider);

Custom providers

You can implement your own provider (e.g. canvas, WebGL, game engine):

import type { EventProvider } from "";

class MyProvider implements EventProvider<any, any> {
  // implement interface
}

EventProvider Interface

Custom providers implement EventProvider<TElement, TStep> to handle events in different environments:

interface EventProvider<TElement, TStep> {
  onInputStart(handler: InputHandler<TStep>, signal: AbortSignal): void;
  onInputEnd(handler: InputHandler<TStep>, signal: AbortSignal): void;
  onPointer(handler: PointerHandler<TElement>, signal: AbortSignal): void;
  observe(
    element: TElement,
    callback: (entries: Entry<TElement>[]) => void,
    signal: AbortSignal,
  ): void;
  unobserve(element: TElement): void;
  getParentElement(element: TElement): TElement | null;
  getIsElementConnected(element: TElement): boolean;
}
  • Allows InputsManager to work with browser DOM, SSR, or test environments
  • Supports lazy initialization for SSR or deferred providers
  • Handlers are automatically queued until the provider is ready

API

InputsManager

createInputsManager(provider?)

Creates a new instance with a specific EventProvider.

const inputsManager = createInputsManager(provider);

register(sequence, setup, element?, scope?)

Registers a steps or stepsSequence.

register(
  Steps | StepsSequence,
  {
    handler: (event) => void;
    clearDuration?: number;
  },
  element?: TElement,
  scope?: string
): ActionId

unregister(id)

Removes a previously registered handlers.


destroy()

Cleans up all subscriptions.


LazyEventProvider

new LazyEventProvider(providerFactory)

Creates new instance with a provider factory

const factory = () => {
  import("./CustomEventProvider").then((m) => new m.CustomEventProvider());
};
const provider = new LazyEventProvider(factory);

isReady: boolean

Indicates whether the provider has already been initialized or not.


reinitialize()

Restarts the provider initialization.


Event Object

Handler receives:

{
  stopPropagation: () => void;
  sequence: Sequence;
  activeSteps: Set;
  timestamp: number;
}

SSR Notes

  • DOMKeyboardEventProvider is not SSR-safe
  • Use LazyEventProvider for:
    • SSR frameworks (Next.js, Nuxt)
    • dynamic imports
    • delayed initialization

Example

import { createInputsManager } from "@hotora/inputs";

const modal = document.getElementById("modal");

const inputsManager = createInputsManager();

inputsManager.register(
  [Keys.Escape],
  {
    handler: () => console.log("Close modal"),
  },
  modal,
  "modal",
);

inputsManager.register([Keys.Escape], {
  handler: () => console.log("Global escape fallback"),
});

License

MIT