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.
A demo showing
⌘ + ArrowUp
,⌘ + ArrowDown
, andF2
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>
);
};
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;
✅ 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)