Dynamic Hotkey Registration
Register hotkeys based on configuration or user preferences:import { useHotkeys } from 'react-hotkeys-hook'
interface HotkeyConfig {
key: string
action: () => void
description: string
}
function DynamicHotkeys({ configs }: { configs: HotkeyConfig[] }) {
configs.forEach(({ key, action }) => {
useHotkeys(key, action)
})
return <div>Hotkeys registered</div>
}
// Usage
const shortcuts = [
{ key: 'ctrl+s', action: handleSave, description: 'Save' },
{ key: 'ctrl+p', action: handlePrint, description: 'Print' },
{ key: 'ctrl+f', action: handleFind, description: 'Find' },
]
<DynamicHotkeys configs={shortcuts} />
User-Customizable Shortcuts
import { useHotkeys } from 'react-hotkeys-hook'
import { useState } from 'react'
function CustomizableShortcuts() {
const [shortcuts, setShortcuts] = useState({
save: 'ctrl+s',
copy: 'ctrl+c',
paste: 'ctrl+v',
})
useHotkeys(shortcuts.save, handleSave)
useHotkeys(shortcuts.copy, handleCopy)
useHotkeys(shortcuts.paste, handlePaste)
const updateShortcut = (action: string, newKey: string) => {
setShortcuts(prev => ({ ...prev, [action]: newKey }))
}
return (
<div>
<h3>Customize Shortcuts</h3>
<input
placeholder="Save shortcut"
value={shortcuts.save}
onChange={(e) => updateShortcut('save', e.target.value)}
/>
</div>
)
}
Command Palette Pattern
import { useHotkeys, useHotkeysContext } from 'react-hotkeys-hook'
import { useState } from 'react'
function CommandPalette() {
const [isOpen, setIsOpen] = useState(false)
const [query, setQuery] = useState('')
const { hotkeys } = useHotkeysContext()
useHotkeys('ctrl+k', () => setIsOpen(true), {
preventDefault: true,
})
useHotkeys('escape', () => setIsOpen(false), {
enabled: isOpen,
})
const filteredCommands = hotkeys.filter(hotkey =>
hotkey.description?.toLowerCase().includes(query.toLowerCase())
)
if (!isOpen) return null
return (
<div className="command-palette">
<input
autoFocus
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search commands..."
/>
<div className="commands">
{filteredCommands.map((hotkey, index) => (
<div key={index} className="command-item">
<span>{hotkey.description}</span>
<kbd>{hotkey.hotkey}</kbd>
</div>
))}
</div>
</div>
)
}
Keyboard Navigation Lists
import { useHotkeys } from 'react-hotkeys-hook'
import { useState } from 'react'
function NavigableList({ items }: { items: string[] }) {
const [selectedIndex, setSelectedIndex] = useState(0)
// Navigation
useHotkeys('j, down', () => {
setSelectedIndex(i => Math.min(items.length - 1, i + 1))
}, [items.length])
useHotkeys('k, up', () => {
setSelectedIndex(i => Math.max(0, i - 1))
})
useHotkeys('g g', () => setSelectedIndex(0)) // Go to first
useHotkeys('shift+g', () => setSelectedIndex(items.length - 1)) // Go to last
// Selection
useHotkeys('enter', () => handleSelect(items[selectedIndex]), [selectedIndex, items])
return (
<ul>
{items.map((item, index) => (
<li
key={index}
className={index === selectedIndex ? 'selected' : ''}
>
{item}
</li>
))}
</ul>
)
}
Modal Dialog with Hotkey Scope
import { useHotkeys, useHotkeysContext } from 'react-hotkeys-hook'
import { useEffect } from 'react'
function Modal({ isOpen, onClose, children }: ModalProps) {
const { enableScope, disableScope } = useHotkeysContext()
useEffect(() => {
if (isOpen) {
enableScope('modal')
} else {
disableScope('modal')
}
return () => disableScope('modal')
}, [isOpen, enableScope, disableScope])
// Modal-specific hotkeys
useHotkeys('escape', onClose, { scopes: 'modal' })
useHotkeys('ctrl+w', onClose, { scopes: 'modal' })
if (!isOpen) return null
return (
<div className="modal-overlay">
<div className="modal-content">
{children}
</div>
</div>
)
}
Vim-Style Editor Modes
import { useHotkeys, useHotkeysContext } from 'react-hotkeys-hook'
import { useState } from 'react'
type Mode = 'normal' | 'insert' | 'visual'
function VimEditor() {
const [mode, setMode] = useState<Mode>('normal')
const { enableScope, disableScope } = useHotkeysContext()
const switchMode = (newMode: Mode) => {
disableScope(mode)
enableScope(newMode)
setMode(newMode)
}
// Normal mode
useHotkeys('i', () => switchMode('insert'), { scopes: 'normal' })
useHotkeys('v', () => switchMode('visual'), { scopes: 'normal' })
useHotkeys('d d', () => deleteLine(), { scopes: 'normal' })
useHotkeys('y y', () => yankLine(), { scopes: 'normal' })
// Insert mode
useHotkeys('escape', () => switchMode('normal'), { scopes: 'insert' })
// Visual mode
useHotkeys('escape', () => switchMode('normal'), { scopes: 'visual' })
useHotkeys('d', () => deleteSelection(), { scopes: 'visual' })
return (
<div>
<div>Mode: {mode}</div>
<textarea />
</div>
)
}
Debounced Hotkeys
import { useHotkeys } from 'react-hotkeys-hook'
import { useCallback, useRef } from 'react'
function useDebounceHotkey(
keys: string,
callback: () => void,
delay: number = 300
) {
const timeoutRef = useRef<NodeJS.Timeout>()
const debouncedCallback = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
timeoutRef.current = setTimeout(callback, delay)
}, [callback, delay])
useHotkeys(keys, debouncedCallback)
}
// Usage
function SearchComponent() {
useDebounceHotkey('ctrl+f', () => {
performSearch()
}, 500)
}
Hotkey Chaining
import { useHotkeys } from 'react-hotkeys-hook'
import { useState } from 'react'
function HotkeyChaining() {
const [lastKey, setLastKey] = useState<string | null>(null)
// g + g = go to top
useHotkeys('g', () => setLastKey('g'))
useHotkeys('g', () => {
if (lastKey === 'g') {
scrollToTop()
setLastKey(null)
}
}, [lastKey])
// Reset after delay
useEffect(() => {
if (lastKey) {
const timeout = setTimeout(() => setLastKey(null), 1000)
return () => clearTimeout(timeout)
}
}, [lastKey])
}
Global Hotkey Help
import { useHotkeys, useHotkeysContext } from 'react-hotkeys-hook'
import { useState } from 'react'
function HotkeyHelp() {
const [isVisible, setIsVisible] = useState(false)
const { hotkeys } = useHotkeysContext()
useHotkeys('shift+/', () => setIsVisible(v => !v))
if (!isVisible) return null
// Group by scope
const groupedHotkeys = hotkeys.reduce((acc, hotkey) => {
const scope = hotkey.scopes?.[0] || 'global'
if (!acc[scope]) acc[scope] = []
acc[scope].push(hotkey)
return acc
}, {} as Record<string, typeof hotkeys>)
return (
<div className="hotkey-help">
<h2>Keyboard Shortcuts</h2>
{Object.entries(groupedHotkeys).map(([scope, keys]) => (
<div key={scope}>
<h3>{scope}</h3>
<table>
<tbody>
{keys.map((hotkey, index) => (
<tr key={index}>
<td><kbd>{hotkey.hotkey}</kbd></td>
<td>{hotkey.description || 'No description'}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
)
}
Conditional Shortcut Overlays
import { useHotkeys, isHotkeyPressed } from 'react-hotkeys-hook'
import { useState, useEffect } from 'react'
function ShortcutOverlay() {
const [showHints, setShowHints] = useState(false)
// Show hints when holding Ctrl
useEffect(() => {
const interval = setInterval(() => {
setShowHints(isHotkeyPressed('ctrl'))
}, 100)
return () => clearInterval(interval)
}, [])
useHotkeys('ctrl+1', () => handleAction1())
useHotkeys('ctrl+2', () => handleAction2())
return (
<div>
<button>
Action 1
{showHints && <span className="hint">Ctrl+1</span>}
</button>
<button>
Action 2
{showHints && <span className="hint">Ctrl+2</span>}
</button>
</div>
)
}
Platform-Specific Shortcuts
import { useHotkeys } from 'react-hotkeys-hook'
function PlatformShortcuts() {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0
const modifier = isMac ? 'cmd' : 'ctrl'
useHotkeys(`${modifier}+s`, handleSave, {
preventDefault: true,
})
useHotkeys(`${modifier}+k`, handleCommand, {
preventDefault: true,
})
return (
<div>
Press {isMac ? '⌘' : 'Ctrl'}+S to save
</div>
)
}
Hotkey Recording
import { useRecordHotkeys } from 'react-hotkeys-hook'
import { useState } from 'react'
function HotkeyRecorder() {
const [recordedKeys, { start, stop, isRecording }] = useRecordHotkeys()
const [savedHotkey, setSavedHotkey] = useState<string>('')
const handleSave = () => {
const keys = Array.from(recordedKeys).join('+')
setSavedHotkey(keys)
stop()
}
return (
<div>
<button onClick={isRecording ? stop : start}>
{isRecording ? 'Stop Recording' : 'Start Recording'}
</button>
{isRecording && (
<div>
<p>Press your desired key combination...</p>
<p>Current: {Array.from(recordedKeys).join('+')}</p>
<button onClick={handleSave}>Save</button>
</div>
)}
{savedHotkey && <p>Saved hotkey: {savedHotkey}</p>}
</div>
)
}
Temporary Hotkey Override
import { useHotkeys, useHotkeysContext } from 'react-hotkeys-hook'
import { useEffect } from 'react'
function TemporaryOverride({ active }: { active: boolean }) {
const { enableScope, disableScope } = useHotkeysContext()
useEffect(() => {
if (active) {
enableScope('override')
} else {
disableScope('override')
}
}, [active, enableScope, disableScope])
// Override global shortcuts when active
useHotkeys('ctrl+s', handleSpecialSave, {
scopes: 'override',
preventDefault: true,
})
return null
}
Combine multiple patterns to build sophisticated keyboard-driven interfaces. The key is organizing hotkeys with scopes and using the context API effectively.
When building complex hotkey systems, always provide a help dialog (Shift+?) so users can discover available shortcuts.
