just-hotkeys
v0.1.0
Published
A declarative keyboard shortcut manager for JavaScript and TypeScript - framework agnostic with React support
Maintainers
Readme
🚀 just-hotkeys
A modern, declarative keyboard shortcut manager for JavaScript and TypeScript. Framework-agnostic with first-class React support.
✨ Features
- 🎯 Declarative API - Just map keys to functions
- 🔧 Framework Agnostic - Works everywhere (vanilla JS, React, Vue, etc.)
- ⚛️ React Hooks -
useShortcuts()for React users - 🖥️ Cross-platform - Handles Cmd/Ctrl automatically
- 📦 TypeScript First - Full type safety out of the box
- 🪶 Lightweight - Zero dependencies, tiny bundle size
- ♿ Accessible - Smart input handling and focus management
🔥 Why Declarative Shortcuts?
Most keyboard shortcut libraries require imperative setup:
// 😰 The old way
hotkeys('cmd+k', event => {
event.preventDefault();
openSearch();
});
hotkeys('cmd+/', event => {
event.preventDefault();
toggleHelp();
});
hotkeys('esc', event => {
event.preventDefault();
closeModal();
});just-hotkeys lets you just declare what you want:
// 🎉 The declarative way
createShortcuts({
'cmd+k': openSearch,
'cmd+/': toggleHelp,
esc: closeModal,
});No manual event handling, no repetitive code, just pure intent.
📦 Installation
npm install just-hotkeys
# or
yarn add just-hotkeys
# or
pnpm add just-hotkeys🚀 Quick Start
Vanilla JavaScript/TypeScript
import { createShortcuts } from 'just-hotkeys';
// Create shortcuts
const cleanup = createShortcuts({
'cmd+k': () => openSearch(),
'cmd+/': () => toggleHelp(),
esc: () => closeModal(),
'ctrl+shift+p': () => openCommandPalette(),
});
// Cleanup when done
cleanup();React
import { useShortcuts } from 'just-hotkeys/react';
function App() {
const [searchOpen, setSearchOpen] = useState(false);
const [helpOpen, setHelpOpen] = useState(false);
useShortcuts({
'cmd+k': () => setSearchOpen(true),
'cmd+/': () => setHelpOpen(!helpOpen),
esc: () => {
setSearchOpen(false);
setHelpOpen(false);
},
});
return (
<div>
{searchOpen && <SearchModal />}
{helpOpen && <HelpPanel />}
</div>
);
}🎮 Supported Key Patterns
All combinations work intuitively:
createShortcuts({
// Single keys
k: openSearch,
esc: closeModal,
enter: submit,
// Modifier combinations
'cmd+k': openSearch, // ⌘K on Mac, Ctrl+K on Windows/Linux
'ctrl+k': forceCtrlK, // Ctrl+K on all platforms
'alt+enter': newWindow, // Alt+Enter
'shift+?': showHelp, // Shift+?
// Multiple modifiers
'cmd+shift+p': commandPalette,
'ctrl+alt+delete': emergencyExit,
// Function keys
f1: help,
f5: refresh,
f11: fullscreen,
// Arrow keys
up: moveUp,
down: moveDown,
'ctrl+up': jumpToTop,
// Special keys
space: playPause,
tab: nextField,
backspace: goBack,
});Key Aliases
Use whatever feels natural:
// These all work the same:
('cmd+k' === 'command+k') === 'meta+k';
('opt+k' === 'option+k') === 'alt+k';
'ctrl+k' === 'control+k';
'del' === 'delete';
'esc' === 'escape';
'space' === ' ';⚙️ Configuration Options
interface ShortcutOptions {
target?: EventTarget; // Element to listen on (default: document)
preventDefault?: boolean; // Prevent default behavior (default: true)
stopPropagation?: boolean; // Stop event bubbling (default: false)
enableInInputs?: boolean; // Work in input fields (default: false)
}
// Example with options
createShortcuts(
{
'cmd+k': openSearch,
},
{
target: myElement,
preventDefault: false,
enableInInputs: true,
}
);🔧 Advanced Usage
Dynamic Shortcut Management
const manager = createShortcuts({
'cmd+k': openSearch,
});
// Add more shortcuts later
manager.add({
'cmd+/': toggleHelp,
esc: closeModal,
});
// Remove specific shortcuts
manager.remove(['cmd+/', 'esc']);
// See what's active
console.log(manager.getActiveShortcuts()); // ['cmd+k']
// Cleanup everything
manager.destroy();React Advanced Hooks
import {
useShortcuts,
useConditionalShortcuts,
useScopedShortcuts,
useShortcutManager,
} from 'just-hotkeys/react';
function AdvancedComponent() {
const [mode, setMode] = useState('normal');
const panelRef = useRef<HTMLDivElement>(null);
// Basic shortcuts
useShortcuts({
'cmd+k': openSearch,
esc: closeEverything,
});
// Conditional shortcuts (only when in edit mode)
useConditionalShortcuts(
{
'cmd+s': save,
'cmd+z': undo,
},
mode === 'edit'
);
// Scoped shortcuts (only when panel is focused)
useScopedShortcuts(
{
j: moveDown,
k: moveUp,
},
panelRef
);
// Full control
const { addShortcuts, removeShortcuts } = useShortcutManager();
useEffect(() => {
if (mode === 'power-user') {
addShortcuts({
'g g': goToTop,
'g i': goToInbox,
});
}
}, [mode]);
return <div ref={panelRef}>...</div>;
}🎨 Real-World Examples
Search Modal
function SearchModal() {
const [open, setOpen] = useState(false);
const [query, setQuery] = useState('');
useShortcuts({
'cmd+k': () => setOpen(true),
esc: () => setOpen(false),
});
return open ? (
<div className="modal">
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
</div>
) : null;
}Command Palette
function CommandPalette() {
const [open, setOpen] = useState(false);
const commands = [
{ key: 'cmd+n', label: 'New File', action: newFile },
{ key: 'cmd+o', label: 'Open File', action: openFile },
{ key: 'cmd+s', label: 'Save', action: save },
];
useShortcuts({
'cmd+shift+p': () => setOpen(true),
esc: () => setOpen(false),
// Register all command shortcuts
...commands.reduce(
(acc, cmd) => ({
...acc,
[cmd.key]: cmd.action,
}),
{}
),
});
return open ? (
<div className="command-palette">
{commands.map(cmd => (
<div key={cmd.key} className="command">
<span className="shortcut">{cmd.key}</span>
<span className="label">{cmd.label}</span>
</div>
))}
</div>
) : null;
}Game Controls
function GameComponent() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useShortcuts({
w: () => setPosition(p => ({ ...p, y: p.y - 1 })),
s: () => setPosition(p => ({ ...p, y: p.y + 1 })),
a: () => setPosition(p => ({ ...p, x: p.x - 1 })),
d: () => setPosition(p => ({ ...p, x: p.x + 1 })),
'shift+w': () => setPosition(p => ({ ...p, y: p.y - 5 })),
space: () => jump(),
});
return <div style={{ left: position.x, top: position.y }}>Player</div>;
}📚 API Reference
Core Functions
createShortcuts(shortcuts, options?)
Creates a shortcut manager instance.
function createShortcuts(
shortcuts: ShortcutMap,
options?: ShortcutOptions
): ShortcutManager;Parameters:
shortcuts: Object mapping shortcut strings to callback functionsoptions: Optional configuration (see ShortcutOptions below)
Returns: ShortcutManager instance with methods for dynamic management
shortcuts(shortcuts, options?)
Simple utility for one-time shortcut creation with automatic cleanup.
function shortcuts(
shortcuts: ShortcutMap,
options?: ShortcutOptions
): () => void;Returns: Cleanup function to remove shortcuts
React Hooks
useShortcuts(shortcuts, options?)
React hook for declarative shortcuts with automatic cleanup.
function useShortcuts(shortcuts: ShortcutMap, options?: ShortcutOptions): void;useConditionalShortcuts(shortcuts, condition, options?)
Hook for shortcuts that only activate under certain conditions.
function useConditionalShortcuts(
shortcuts: ShortcutMap,
condition: boolean,
options?: ShortcutOptions
): void;useScopedShortcuts(shortcuts, targetRef, options?)
Hook for shortcuts scoped to a specific DOM element.
function useScopedShortcuts(
shortcuts: ShortcutMap,
targetRef: React.RefObject<HTMLElement>,
options?: Omit<ShortcutOptions, 'target'>
): void;useShortcutManager(options?)
Hook for full control over shortcut lifecycle.
function useShortcutManager(options?: ShortcutOptions): {
addShortcuts: (shortcuts: ShortcutMap) => void;
removeShortcuts: (keys: string[]) => void;
getActiveShortcuts: () => string[];
};Types
ShortcutMap
type ShortcutMap = Record<string, ShortcutCallback>;ShortcutCallback
type ShortcutCallback = (event: KeyboardEvent) => void;ShortcutOptions
interface ShortcutOptions {
target?: EventTarget; // Element to listen on (default: document)
preventDefault?: boolean; // Prevent default behavior (default: true)
stopPropagation?: boolean; // Stop event bubbling (default: false)
enableInInputs?: boolean; // Work in input fields (default: false)
}ShortcutManager
interface ShortcutManager {
add: (shortcuts: ShortcutMap) => void;
remove: (keys: string[]) => void;
destroy: () => void;
getActiveShortcuts: () => string[];
}🧪 Testing
# Run tests
npm test
# Run tests with UI
npm run test:ui
# Type checking
npm run type-check
# Linting
npm run lint🔧 Development
# Install dependencies
npm install
# Start dev server
npm run dev
# Build for production
npm run build
# Preview build
npm run preview🤝 Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
🙏 Acknowledgments
- Inspired by the need for simpler keyboard shortcut management
- Built with modern web standards and TypeScript
- Thanks to all contributors and users
Made with ❤️ for developers who just want their shortcuts to work.
