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

@nkemtasoft-react/keybindings

v1.0.2

Published

A customizable React hook for handling keyboard events. Features dynamic binding management, modifier key support, input field prevention, and is built with TypeScript for excellent developer experience.

Readme

🎹 useKeyboardBindings

npm version codecov Build Status TypeScript Bundle Size License: MIT

A powerful, lightweight React hook for managing dynamic keyboard shortcuts and key bindings with TypeScript support. Perfect for building keyboard-driven applications, shortcuts, and hotkey systems.

✨ Features

  • 🚀 Dynamic Management - Add, remove, update, enable/disable bindings at runtime
  • ⌨️ Full Modifier Support - Ctrl, Shift, Alt, Meta (Cmd) keys with smart aliases
  • 🎯 Smart Input Filtering - Automatically prevents conflicts in text inputs/textareas
  • 🎨 Flexible Targeting - Document-wide or specific element targeting via refs
  • 🔧 Event Control - Fine-grained preventDefault and stopPropagation control
  • 🌐 Global Controls - Enable/disable all bindings with a single toggle
  • 📝 TypeScript First - Complete type safety and IntelliSense support
  • 🪶 Lightweight - Under 4KB minified, zero dependencies (except React)
  • Optimized Performance - Efficient event handling with minimal re-renders
  • 🔍 Developer Friendly - Comprehensive debugging and introspection methods

📦 Installation

npm install @nkemtasoft-react/keybindings
yarn add @nkemtasoft-react/keybindings
pnpm add @nkemtasoft-react/keybindings

🚀 Quick Start

import React, { useState } from "react";
import { useKeyboardBindings } from "@nkemtasoft-react/keybindings";

function MyApp() {
    const [message, setMessage] = useState("");

    const { addBinding, removeBinding } = useKeyboardBindings({
        // Initial bindings
        "ctrl+s": () => setMessage("Saved!"),
        "ctrl+n": () => setMessage("New document"),
        esc: () => setMessage(""),
    });

    // Add bindings dynamically
    const addCustomBinding = () => {
        addBinding("ctrl+k", () => setMessage("Custom shortcut!"));
    };

    return (
        <div>
            <p>{message}</p>
            <button onClick={addCustomBinding}>Add Ctrl+K shortcut</button>
            <p>Try: Ctrl+S, Ctrl+N, or Escape</p>
        </div>
    );
}

📚 Advanced Examples

🎯 Text Editor with Multiple Shortcuts

import React, { useState, useRef } from "react";
import { useKeyboardBindings } from "@nkemtasoft-react/keybindings";

function TextEditor() {
    const [content, setContent] = useState("");
    const [saved, setSaved] = useState(false);
    const editorRef = useRef<HTMLDivElement>(null);

    const { addBinding, disableAllBindings, enableAllBindings, bindings } = useKeyboardBindings(
        {
            // File operations
            "ctrl+s": () => {
                console.log("Saving...", content);
                setSaved(true);
                setTimeout(() => setSaved(false), 2000);
            },
            "ctrl+n": () => {
                setContent("");
                setSaved(false);
            },
            "ctrl+o": () => {
                // Open file logic
                console.log("Opening file...");
            },

            // Editing operations
            "ctrl+z": () => console.log("Undo"),
            "ctrl+y": () => console.log("Redo"),
            "ctrl+a": () => console.log("Select all"),

            // Navigation
            "ctrl+g": () => console.log("Go to line"),
            "ctrl+f": () => console.log("Find"),
            f3: () => console.log("Find next"),

            // View
            "ctrl+=": () => console.log("Zoom in"),
            "ctrl+-": () => console.log("Zoom out"),
            f11: () => console.log("Toggle fullscreen"),
        },
        {
            // Global options
            preventDefault: true,
            target: editorRef, // Scope to editor only
        },
    );

    return (
        <div ref={editorRef} className="editor">
            <div className="toolbar">
                <span className={saved ? "saved" : "unsaved"}>{saved ? "✅ Saved" : "⚠️ Unsaved"}</span>
                <button onClick={() => disableAllBindings()}>Disable Shortcuts</button>
                <button onClick={() => enableAllBindings()}>Enable Shortcuts</button>
            </div>

            <textarea
                value={content}
                onChange={(e) => setContent(e.target.value)}
                placeholder="Start typing... (shortcuts work outside this textarea)"
                className="content"
            />

            <div className="shortcuts">
                <h3>Active Shortcuts:</h3>
                {Object.entries(bindings).map(([combo, binding]) => (
                    <span key={combo} className={`shortcut ${binding.enabled ? "enabled" : "disabled"}`}>
                        {combo}
                    </span>
                ))}
            </div>
        </div>
    );
}

🎮 Game Controls with Dynamic Binding

import React, { useState, useEffect } from "react";
import { useKeyboardBindings } from "@nkemtasoft-react/keybindings";

function GameController() {
    const [score, setScore] = useState(0);
    const [gameState, setGameState] = useState<"menu" | "playing" | "paused">("menu");

    const { addBinding, removeBinding, updateBinding, disableAllBindings, enableAllBindings } = useKeyboardBindings();

    // Dynamic controls based on game state
    useEffect(() => {
        if (gameState === "menu") {
            addBinding("space", () => setGameState("playing"));
            addBinding("enter", () => setGameState("playing"));
            removeBinding("p"); // Remove pause in menu
        } else if (gameState === "playing") {
            // Game controls
            addBinding("w", () => console.log("Move up"));
            addBinding("a", () => console.log("Move left"));
            addBinding("s", () => console.log("Move down"));
            addBinding("d", () => console.log("Move right"));
            addBinding("space", () => console.log("Jump/Action"));
            addBinding("p", () => setGameState("paused"));
            addBinding("esc", () => setGameState("menu"));
        } else if (gameState === "paused") {
            disableAllBindings(); // Disable game controls
            addBinding("p", () => setGameState("playing"));
            addBinding("esc", () => setGameState("menu"));
        }
    }, [gameState]);

    // Power-up: temporarily change controls
    const activatePowerUp = () => {
        updateBinding("space", () => {
            console.log("SUPER JUMP!");
            setScore((prev) => prev + 100);
        });

        // Revert after 5 seconds
        setTimeout(() => {
            updateBinding("space", () => console.log("Normal jump"));
        }, 5000);
    };

    return (
        <div className="game">
            <div className="hud">
                <div>Score: {score}</div>
                <div>State: {gameState}</div>
            </div>

            {gameState === "menu" && (
                <div className="menu">
                    <h1>Game Menu</h1>
                    <p>Press SPACE or ENTER to start</p>
                </div>
            )}

            {gameState === "playing" && (
                <div className="game-area">
                    <p>Use WASD to move, SPACE to jump</p>
                    <p>Press P to pause, ESC for menu</p>
                    <button onClick={activatePowerUp}>Activate Super Jump</button>
                </div>
            )}

            {gameState === "paused" && (
                <div className="pause-menu">
                    <h2>PAUSED</h2>
                    <p>Press P to continue, ESC for menu</p>
                </div>
            )}
        </div>
    );
}

🔧 Advanced Configuration

import { useKeyboardBindings } from "@nkemtasoft-react/keybindings";

function AdvancedExample() {
    const containerRef = useRef<HTMLDivElement>(null);

    const { addBinding, updateBinding, getBinding, hasBinding, toggleBinding } = useKeyboardBindings(
        {
            // Initial bindings with individual options
            "ctrl+shift+d": () => console.log("Debug mode"),
        },
        {
            // Global options
            preventDefault: false, // Don't prevent default globally
            stopPropagation: true, // Stop propagation globally
            disableSingleFromTextInput: false, // Allow single keys in inputs
            target: containerRef, // Scope to specific element
            globalDisabled: false, // Start enabled
        },
    );

    // Add binding with specific options
    const addCustomBinding = () => {
        addBinding(
            "alt+enter",
            (event) => {
                console.log("Custom action with event:", event);
            },
            {
                preventDefault: true, // Override global setting
                stopPropagation: false, // Override global setting
                disableSingleFromTextInput: true, // Don't trigger in inputs
            },
        );
    };

    // Conditional binding management
    const toggleDebugMode = () => {
        if (hasBinding("f12")) {
            removeBinding("f12");
        } else {
            addBinding("f12", () => {
                const binding = getBinding("ctrl+shift+d");
                console.log("Debug binding enabled:", binding?.enabled);
            });
        }
    };

    return (
        <div ref={containerRef}>
            <button onClick={addCustomBinding}>Add Alt+Enter</button>
            <button onClick={toggleDebugMode}>Toggle F12 Debug</button>
            <button onClick={() => toggleBinding("ctrl+shift+d")}>Toggle Debug Binding</button>
        </div>
    );
}

📖 API Reference

Hook Signature

const bindings = useKeyboardBindings(
  initialBindings?: Record<string, (event: KeyboardEvent) => void>,
  options?: HookOptions
);

Options

interface HookOptions {
    preventDefault?: boolean; // Default: true
    stopPropagation?: boolean; // Default: false
    disableSingleFromTextInput?: boolean; // Default: true
    target?: Document | React.RefObject<HTMLElement>; // Default: document
    globalDisabled?: boolean; // Default: false
}

interface BindingOptions {
    preventDefault?: boolean;
    stopPropagation?: boolean;
    disableSingleFromTextInput?: boolean;
}

Return Value

The hook returns an object with these methods and properties:

Binding Management

// Add a new binding
addBinding(combination: string, callback: (event: KeyboardEvent) => void, options?: BindingOptions): void;

// Remove a binding
removeBinding(combination: string): void;

// Update existing binding or create new one
updateBinding(combination: string, callback: (event: KeyboardEvent) => void, options?: BindingOptions): void;

Enable/Disable Controls

// Enable specific binding
enableBinding(combination: string): void;

// Disable specific binding
disableBinding(combination: string): void;

// Toggle binding state
toggleBinding(combination: string): void;

// Bulk operations
enableAllBindings(): void;
disableAllBindings(): void;
clearAllBindings(): void;

Query Methods

// Get binding details
getBinding(combination: string): Binding | null;

// Get all bindings
getAllBindings(): Record<string, Binding>;

// Check if binding exists
hasBinding(combination: string): boolean;

// Read-only bindings object (without callbacks)
bindings: Record<string, Omit<Binding, "callback">>;

Key Combination Format

The hook supports various key combination formats:

Modifier Keys

  • ctrl or control - Control key
  • shift - Shift key
  • alt - Alt key
  • meta, cmd, or command - Meta/Command key (⌘ on Mac)

Special Keys

  • space - Space bar
  • esc or escape - Escape key
  • enter - Enter key
  • tab - Tab key
  • up, down, left, right - Arrow keys
  • f1 through f12 - Function keys

Examples

"ctrl+s"; // Ctrl + S
"cmd+shift+z"; // Cmd + Shift + Z (Mac)
"alt+f4"; // Alt + F4
"ctrl+shift+alt+d"; // All modifiers + D
"space"; // Just spacebar
"esc"; // Just escape
"f11"; // Just F11
"+"; // Plus key (special handling)
"ctrl++"; // Ctrl + Plus

🎯 Key Features Explained

🛡️ Smart Input Filtering

By default, single key bindings (without modifiers) are disabled in text inputs to prevent interference with typing:

// ✅ These work in text inputs (have modifiers)
"ctrl+s"; // Save
"alt+tab"; // Switch
"meta+c"; // Copy

// ❌ These are blocked in text inputs (no modifiers)
"space"; // Would interfere with typing
"enter"; // Would interfere with form submission
"a"; // Would interfere with typing

// 🔧 Override per binding
addBinding("enter", handleSubmit, {
    disableSingleFromTextInput: false, // Allow in inputs
});

🎯 Flexible Targeting

Scope bindings to specific elements:

const editorRef = useRef<HTMLDivElement>(null);

// Only listen for events within the editor
const { addBinding } = useKeyboardBindings(
    {},
    {
        target: editorRef,
    },
);

// Document-wide (default)
const { addBinding } = useKeyboardBindings(
    {},
    {
        target: document, // or omit for default
    },
);

⚡ Performance Optimized

  • Minimal re-renders - Callbacks are memoized and stable
  • Efficient event handling - Single event listener with smart matching
  • Memory management - Proper cleanup on unmount
  • Bundle size - Under 4KB minified

🔧 Event Control

Fine-grained control over event behavior:

addBinding("ctrl+s", handleSave, {
    preventDefault: true, // Prevent browser save dialog
    stopPropagation: false, // Allow event to bubble
});

🧪 Testing

The library includes comprehensive test coverage:

npm test              # Run tests
npm run test:coverage # Coverage report
npm run test:watch    # Watch mode

Test Coverage

  • 98%+ Code Coverage across all features
  • Unit Tests for all hook methods
  • Integration Tests with React components
  • Performance Tests for large binding sets
  • Edge Case Testing for error scenarios

🏗️ Browser Support

  • ✅ Chrome 61+
  • ✅ Firefox 60+
  • ✅ Safari 10.1+
  • ✅ Edge 16+
  • ✅ React 16.8+ (Hooks support)

Development Setup

git clone https://github.com/nkemtasoft-react/keybindings
cd keybindings
npm install

# Development
npm run test:watch    # Run tests in watch mode
npm run build     # Build development version

📄 License

MIT © Nkemtasoft


🌟 Star History

If this library helps you build awesome keyboard-driven experiences, please consider giving it a star! ⭐

Star History Chart


  • Bundle Size: < 4KB minified
  • Dependencies: Zero (except React peer dependency)
  • TypeScript: Full support with complete type definitions
  • Test Coverage: 98%+
  • Build: Dual CJS/ESM output with source maps
  • Performance: Optimized for 1000+ bindings