@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
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/keybindingsyarn add @nkemtasoft-react/keybindingspnpm 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
ctrlorcontrol- Control keyshift- Shift keyalt- Alt keymeta,cmd, orcommand- Meta/Command key (⌘ on Mac)
Special Keys
space- Space barescorescape- Escape keyenter- Enter keytab- Tab keyup,down,left,right- Arrow keysf1throughf12- 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 modeTest 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! ⭐
- 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
