useKeyBinding

useKeyBinding registers a keydown listener and runs your callback when the requested key combination matches.

When to use

  • Add shortcuts like Meta + k for command palette toggles.
  • Trigger actions from keyboard-only workflows.
  • Scope shortcuts to a specific element instead of the whole document.

Supported keys

  • Modifier keys: Meta, Ctrl/Control, Alt, Shift.
  • Regular keys: any KeyboardEvent.key value (for example Enter, k, Escape).
  • Combinations: pass modifiers and regular keys together.

Example

useKeyBinding(['Meta', 'k'], event => {
  event.preventDefault();
  openCommandPalette();
});

const containerRef = useRef<HTMLDivElement>(null);

useKeyBinding(
  ['Enter'],
  () => {
    submitSelection();
  },
  containerRef.current
);

Usage

import { useState } from 'react';

import { useKeyBinding } from '@branditdev/hooks';

export function UseKeyBindingDemo() {
  const [count, setCount] = useState(0);

  useKeyBinding(['Meta', 'k'], () => {
    setCount(current => current + 1);
  });

  return (
    <div>
      <p>Press Cmd+K (or Windows key+K) to trigger the shortcut.</p>
      <p>Shortcut triggered: {count}</p>
    </div>
  );
}

API

useKeyBinding(keys, callback, node?): void

Defined in: effects/useKeyBinding/useKeyBinding.ts:28

Registers a keyboard shortcut and executes a callback on match.

The hook supports modifier keys (Meta, Ctrl/Control, Alt, Shift), regular keys, and combinations of both.

Parameters

NameTypeOptionalDefaultDescription
keysstring[]no-Key definition for the shortcut. Example: ['Meta', 'k'].
callback(event) => voidno-Handler invoked when the key combination matches.
nodeNode | nullyes-Optional target node to attach the listener to. Defaults to document.

Returns

void

Source

import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';

const MODIFIER_KEYS = {
  Meta: 'metaKey',
  Ctrl: 'ctrlKey',
  Control: 'ctrlKey',
  Alt: 'altKey',
  Shift: 'shiftKey',
} as const;

type ModifierKey = keyof typeof MODIFIER_KEYS;

function isModifierKey(key: string): key is ModifierKey {
  return key in MODIFIER_KEYS;
}

export function useKeyBinding(
  keys: string[],
  callback: (event: KeyboardEvent) => void,
  node: Node | null = null
) {
  // implement the callback ref pattern
  const callbackRef = useRef(callback);
  useLayoutEffect(() => {
    callbackRef.current = callback;
  });

  // handle what happens on key press
  const handleKeyPress = useCallback(
    (event: Event) => {
      if (!(event instanceof KeyboardEvent)) {
        return;
      }

      const e = event;
      // Separate modifier keys from regular keys
      const modifierKeys = keys.filter(isModifierKey);
      const regularKeys = keys.filter(key => !isModifierKey(key));

      // Check if all required modifier keys are pressed
      const modifiersPressed = modifierKeys.every(modifier => {
        const modifierProperty = MODIFIER_KEYS[modifier];
        return e[modifierProperty];
      });

      // Check if one of the regular keys matches (or if no regular keys specified, just check modifiers)
      const keyMatches =
        regularKeys.length === 0 || regularKeys.some(key => e.key === key);

      // Execute callback if all conditions are met
      if (
        modifiersPressed &&
        keyMatches &&
        (modifierKeys.length > 0 || regularKeys.length > 0)
      ) {
        callbackRef.current(e);
      }
    },
    [keys]
  );

  useEffect(() => {
    // target is either the provided node or the document
    const targetNode = node ?? document;
    // attach the event listener
    if (targetNode) targetNode.addEventListener('keydown', handleKeyPress);

    // remove the event listener
    return () =>
      targetNode && targetNode.removeEventListener('keydown', handleKeyPress);
  }, [handleKeyPress, node]);
}