- Регистрация
- 1 Мар 2015
- Сообщения
- 16,069
- Баллы
- 155
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.
This hook lets you:
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
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?
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!
The custom hook useKeyPress allows you to react to combinations like ⌘ + ArrowUp or single keys like F2, in a declarative and reusable way.
What does useKeyPress do?A demo showing ⌘ + ArrowUp, ⌘ + ArrowDown, and F2 controlling a counter.
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.
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;

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.

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 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!
