Skip to main content

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>
  )
}
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.

Build docs developers (and LLMs) love