DEV Community

Erlin Banegas
Erlin Banegas

Posted on

Custom Hooks – useKeyPress

Adding keyboard shortcuts to a web application not only improves the user experience, but also makes certain tasks much faster and more intuitive.

The custom hook useKeyPress allows you to react to combinations like ⌘ + ArrowUp or single keys like F2, in a declarative and reusable way.

Image description

A demo showing ⌘ + ArrowUp, ⌘ + ArrowDown, and F2 controlling a counter.

What does useKeyPress do?

This hook lets you:

  • Listen to multiple key combinations from an array, like ["⌘ + ArrowUp", "F2"].
  • Receive the main key pressed as an argument (e.g., "ArrowUp", "F2").
  • Use a single handler function to manage multiple shortcuts easily with if or switch.

Example: keyboard-controlled counter

import { useState } from "react";
import { useKeyPress } from "../core/hooks";

export const KeyPressComponent = () => {
  const [counter, setCounter] = useState(0);

  useKeyPress(["⌘ + ArrowDown", "⌘ + ArrowUp", "F2"], (key: string) => {
    if (key === "ArrowDown") 
        setCounter((prev) => prev - 1);

    if (key === "ArrowUp") 
        setCounter((prev) => prev + 1);

    if (key === "F2") 
        setCounter(0);
  });

  return (
    <div>
      <h1>Key Press Example</h1>
      <strong>{counter}</strong>
      <p>
        Press <em>⌘ + ArrowUp</em> to increase the counter
      </p>
      <p>
        Press <em>⌘ + ArrowDown</em> to decrease the counter
      </p>
      <p>Press F2 to reset the counter</p>
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

How does it work internally?

Under the hood, useKeyPress listens for global keyboard events, normalizes combinations (e.g., "⌘ + ArrowUp" becomes "meta+arrowup"), and runs your callback only when an exact match is detected. You only get the main key in the callback ("ArrowUp", "F2", etc.), which keeps your component logic clean.

Here’s a full example of the hook implementation:

import { useEffect } from "react";

const useKeyPress = (keys: string[], callback: (key: string) => void) => {
  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      const pressedKey = event.key.toLowerCase();

      const normalizedKeys = keys.map((key) =>
        key
          .toLowerCase()
          .split(" + ")
          .map((k) => k.trim())
      );

      const modifiers = {
        control: event.ctrlKey,
        shift: event.shiftKey,
        alt: event.altKey,
        meta: event.metaKey,
        "⌘": event.ctrlKey,
        ctrl: event.ctrlKey,
      };

      const modifierKeys = [
        "control",
        "shift",
        "alt",
        "meta",
        "⌘",
        "ctrl",
      ] as const;

      for (const combination of normalizedKeys) {
        const requiredModifiers = combination.filter((key) =>
          modifierKeys.includes(key as (typeof modifierKeys)[number])
        );
        const requiredKeys = combination.filter(
          (key) => !modifierKeys.includes(key as (typeof modifierKeys)[number])
        );

        const allModifiersMatch = requiredModifiers.every(
          (mod) => modifiers[mod as keyof typeof modifiers]
        );

        const keyMatch = requiredKeys.includes(pressedKey);

        if (keyMatch && allModifiersMatch) {
          event.preventDefault();
          callback(event.key);
          return;
        }
      }
    };

    document.addEventListener("keydown", handleKeyDown, { capture: true });

    return () => {
      document.removeEventListener("keydown", handleKeyDown, { capture: true });
    };
  }, [keys, callback]);
};

export default useKeyPress;

Enter fullscreen mode Exit fullscreen mode

✅ Benefits

  • ✅ Clear syntax: define multiple combinations in an array.
  • ✅ Clean-up included: automatically removes the listener.
  • ✅ Flexible: works across different platforms (Windows, macOS).
  • ✅ Declarative: avoids manual DOM event handling.

🧠 Final thoughts

Handling keyboard shortcuts doesn’t have to be messy. With useKeyPress, you can keep your components clean while adding power-user features that scale well.

Want to extend it?

  • Ignore events when inside an
  • Add debounce or throttle
  • Support dynamic combos via state

🙌 Thanks for reading

Thanks for reading this post — I hope it was helpful!

Feel free to leave a comment or suggestion below.
You're welcome to try, adapt, and improve the useKeyPress hook to fit your own use case.

Happy coding! 🎯

Top comments (0)