React hook for handling keyboard shortcuts in components in a declarative way
—
Context-based system for organizing hotkeys into groups and controlling their active state across an application, enabling complex hotkey hierarchies and conditional activation.
Context provider for managing hotkey scopes across an application.
/**
* Context provider for managing hotkey scopes across an application
* @param initiallyActiveScopes - Scopes that are active when provider mounts (default: ['*'])
* @param children - React children to wrap with hotkey context
*/
function HotkeysProvider({
initiallyActiveScopes,
children
}: {
initiallyActiveScopes?: string[];
children: ReactNode;
}): JSX.Element;Usage Examples:
import { HotkeysProvider } from 'react-hotkeys-hook';
// Basic provider setup
function App() {
return (
<HotkeysProvider initiallyActiveScopes={['global']}>
<MainContent />
</HotkeysProvider>
);
}
// Multiple initial scopes
function App() {
return (
<HotkeysProvider initiallyActiveScopes={['global', 'navigation']}>
<MainContent />
</HotkeysProvider>
);
}
// Default wildcard scope (matches all hotkeys)
function App() {
return (
<HotkeysProvider>
{/* initiallyActiveScopes defaults to ['*'] */}
<MainContent />
</HotkeysProvider>
);
}Hook to access hotkeys context for scope management and hotkey inspection.
/**
* Hook to access hotkeys context for scope management
* @returns Context object with scope control functions and registered hotkeys
*/
function useHotkeysContext(): HotkeysContextType;
interface HotkeysContextType {
/** Array of currently registered hotkeys */
hotkeys: ReadonlyArray<Hotkey>;
/** Array of currently active scope names */
activeScopes: string[];
/** Toggle a scope's active state */
toggleScope: (scope: string) => void;
/** Enable a specific scope */
enableScope: (scope: string) => void;
/** Disable a specific scope */
disableScope: (scope: string) => void;
}Usage Examples:
import { useHotkeysContext } from 'react-hotkeys-hook';
function ScopeController() {
const { activeScopes, enableScope, disableScope, toggleScope, hotkeys } = useHotkeysContext();
return (
<div>
<p>Active scopes: {activeScopes.join(', ')}</p>
<p>Registered hotkeys: {hotkeys.length}</p>
<button onClick={() => enableScope('editor')}>
Enable Editor Scope
</button>
<button onClick={() => disableScope('navigation')}>
Disable Navigation
</button>
<button onClick={() => toggleScope('debug')}>
Toggle Debug Mode
</button>
</div>
);
}The wildcard scope (*) is special and matches all hotkeys by default.
// These are equivalent when no scopes are specified
useHotkeys('ctrl+k', callback); // Implicitly uses '*' scope
useHotkeys('ctrl+k', callback, { scopes: ['*'] });
// When wildcard is active, scoped hotkeys also work
const { activeScopes } = useHotkeysContext();
// activeScopes: ['*']
useHotkeys('ctrl+j', callback, { scopes: ['editor'] }); // This works
useHotkeys('ctrl+l', callback, { scopes: ['navigation'] }); // This also worksWhen enabling specific scopes, the wildcard scope is automatically replaced.
function ScopeExample() {
const { activeScopes, enableScope } = useHotkeysContext();
// Initially: activeScopes = ['*']
enableScope('editor');
// Now: activeScopes = ['editor'] (wildcard removed)
enableScope('navigation');
// Now: activeScopes = ['editor', 'navigation']
return null;
}Hotkeys can belong to multiple scopes for flexible organization.
// Hotkey active in multiple contexts
useHotkeys('ctrl+s', handleSave, {
scopes: ['editor', 'form', 'global']
});
// Array of scopes
useHotkeys('escape', handleCancel, {
scopes: ['modal', 'dropdown', 'overlay']
});function Modal({ isOpen, onClose, children }) {
const { enableScope, disableScope } = useHotkeysContext();
useEffect(() => {
if (isOpen) {
enableScope('modal');
} else {
disableScope('modal');
}
}, [isOpen, enableScope, disableScope]);
// Modal-specific hotkeys
useHotkeys('escape', onClose, { scopes: ['modal'] });
useHotkeys('ctrl+enter', handleSubmit, { scopes: ['modal'] });
return isOpen ? <div className="modal">{children}</div> : null;
}function CodeEditor() {
const [mode, setMode] = useState('normal');
const { activeScopes, enableScope, disableScope } = useHotkeysContext();
useEffect(() => {
// Clean up previous mode
disableScope('normal');
disableScope('insert');
disableScope('visual');
// Enable current mode
enableScope(mode);
enableScope('editor'); // Always active in editor
}, [mode, enableScope, disableScope]);
// Mode-specific hotkeys
useHotkeys('i', () => setMode('insert'), { scopes: ['normal'] });
useHotkeys('v', () => setMode('visual'), { scopes: ['normal'] });
useHotkeys('escape', () => setMode('normal'), { scopes: ['insert', 'visual'] });
// Global editor hotkeys
useHotkeys('ctrl+s', handleSave, { scopes: ['editor'] });
return <div className="editor">...</div>;
}function App() {
const [debugMode, setDebugMode] = useState(false);
const { toggleScope, activeScopes, hotkeys } = useHotkeysContext();
useEffect(() => {
if (debugMode) {
toggleScope('debug');
}
}, [debugMode, toggleScope]);
// Debug-only hotkeys
useHotkeys('ctrl+shift+d', () => console.log('Debug info'), {
scopes: ['debug']
});
useHotkeys('f12', () => setDebugMode(!debugMode));
if (debugMode) {
return (
<div>
<div>Active scopes: {activeScopes.join(', ')}</div>
<div>Registered hotkeys: {hotkeys.length}</div>
<MainApp />
</div>
);
}
return <MainApp />;
}interface Hotkey extends KeyboardModifiers {
keys?: readonly string[];
scopes?: Scopes;
description?: string;
isSequence?: boolean;
}
interface KeyboardModifiers {
alt?: boolean;
ctrl?: boolean;
meta?: boolean;
shift?: boolean;
mod?: boolean;
useKey?: boolean;
}
type Scopes = string | readonly string[];Install with Tessl CLI
npx tessl i tessl/npm-react-hotkeys-hook