typescriptbeginner

useKeyboardShortcut Hook

Register global keyboard shortcuts with modifier keys, preventing defaults, and cleanup on unmount.

typescript
import { useEffect, useCallback } from 'react';

type ModifierKey = 'ctrl' | 'alt' | 'shift' | 'meta';

interface ShortcutOptions {
  key: string;
  modifiers?: ModifierKey[];
  preventDefault?: boolean;
  enabled?: boolean;
}

function useKeyboardShortcut(
  options: ShortcutOptions,
  callback: (e: KeyboardEvent) => void,
) {
  const {
    key,
    modifiers = [],
    preventDefault = true,
    enabled = true,
  } = options;

  const handler = useCallback(
    (e: KeyboardEvent) => {
      if (!enabled) return;

      const modMatch =
        (!modifiers.includes('ctrl') || e.ctrlKey || e.metaKey) &&
        (!modifiers.includes('meta') || e.metaKey) &&
        (!modifiers.includes('alt') || e.altKey) &&
        (!modifiers.includes('shift') || e.shiftKey);

      if (e.key.toLowerCase() === key.toLowerCase() && modMatch) {
        if (preventDefault) e.preventDefault();
        callback(e);
      }
    },
    [key, modifiers, preventDefault, enabled, callback],
  );

  useEffect(() => {
    if (!enabled) return;
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [handler, enabled]);
}

// Usage Example
function App() {
  useKeyboardShortcut(
    { key: 'k', modifiers: ['ctrl'] },
    () => console.log('Open search'),
  );

  useKeyboardShortcut(
    { key: 's', modifiers: ['ctrl'] },
    () => console.log('Save'),
  );

  useKeyboardShortcut(
    { key: 'Escape' },
    () => console.log('Close modal'),
  );

  return <div>Press Ctrl+K to search, Ctrl+S to save</div>;
}

export { useKeyboardShortcut };

Use Cases

  • Command palette shortcuts
  • Save/undo keyboard commands
  • Modal dismiss with Escape

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.