Keyboard Events
Respond to key presses, releases, and keyboard shortcuts in React components.
Keyboard Event Reference
| React Prop | TypeScript Type | Fires When | Notes |
|---|---|---|---|
onKeyDown | React.KeyboardEvent<T> | A key is pressed down | Primary keyboard event -- use this one |
onKeyUp | React.KeyboardEvent<T> | A key is released | Fires after the key action completes |
onKeyPress | React.KeyboardEvent<T> | A character key is pressed | Deprecated -- do not use in new code |
Recipe
Quick-reference recipe card -- copy-paste ready.
// Enter key to submit
function EnterSubmit() {
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
console.log("Submitted:", e.currentTarget.value);
}
};
return <input onKeyDown={handleKeyDown} placeholder="Press Enter" />;
}
// Escape key to clear / close
function EscapeToClear() {
const [value, setValue] = React.useState("");
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
setValue("");
e.currentTarget.blur();
}
};
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Escape to clear"
/>
);
}
// Keyboard shortcut with modifier (Cmd/Ctrl + S)
function SaveShortcut() {
React.useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
e.preventDefault();
console.log("Save triggered");
}
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, []);
return <div>Press Cmd+S (Mac) or Ctrl+S (Windows) to save</div>;
}When to reach for this: You need to respond to keyboard input -- submitting on Enter, closing on Escape, navigating with arrow keys, or registering global keyboard shortcuts.
Working Example
"use client";
import { useState, useEffect, useRef, useCallback } from "react";
type SearchResult = { id: string; title: string };
const MOCK_RESULTS: SearchResult[] = [
{ id: "1", title: "Getting Started with React" },
{ id: "2", title: "React Hooks in Depth" },
{ id: "3", title: "Server Components Explained" },
{ id: "4", title: "TypeScript with React" },
{ id: "5", title: "React Performance Patterns" },
];
export default function SearchWithShortcuts() {
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const results = query
? MOCK_RESULTS.filter((r) =>
r.title.toLowerCase().includes(query.toLowerCase())
)
: [];
// Global keyboard shortcut: Cmd+K to open search
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
setIsOpen(true);
// Focus after state update renders the input
setTimeout(() => inputRef.current?.focus(), 0);
}
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, []);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setSelectedIndex((i) => Math.min(i + 1, results.length - 1));
break;
case "ArrowUp":
e.preventDefault();
setSelectedIndex((i) => Math.max(i - 1, 0));
break;
case "Enter":
e.preventDefault();
if (results[selectedIndex]) {
console.log("Selected:", results[selectedIndex].title);
setIsOpen(false);
setQuery("");
}
break;
case "Escape":
setIsOpen(false);
setQuery("");
inputRef.current?.blur();
break;
}
},
[results, selectedIndex]
);
if (!isOpen) {
return (
<button
onClick={() => {
setIsOpen(true);
setTimeout(() => inputRef.current?.focus(), 0);
}}
style={{
padding: "8px 16px",
border: "1px solid #e5e7eb",
borderRadius: "8px",
background: "#fff",
cursor: "pointer",
}}
>
Search...{" "}
<kbd style={{ color: "#9ca3af", fontSize: "0.85em" }}>Cmd+K</kbd>
</button>
);
}
return (
<div
style={{
border: "1px solid #e5e7eb",
borderRadius: "12px",
overflow: "hidden",
boxShadow: "0 4px 12px rgba(0,0,0,0.1)",
width: 400,
}}
>
<input
ref={inputRef}
value={query}
onChange={(e) => {
setQuery(e.target.value);
setSelectedIndex(0);
}}
onKeyDown={handleKeyDown}
placeholder="Type to search... (Esc to close)"
style={{
width: "100%",
padding: "12px 16px",
border: "none",
outline: "none",
fontSize: "16px",
boxSizing: "border-box",
}}
/>
{results.length > 0 && (
<ul style={{ listStyle: "none", margin: 0, padding: 0 }}>
{results.map((result, index) => (
<li
key={result.id}
style={{
padding: "10px 16px",
background: index === selectedIndex ? "#f3f4f6" : "#fff",
cursor: "pointer",
}}
onClick={() => {
console.log("Selected:", result.title);
setIsOpen(false);
setQuery("");
}}
>
{result.title}
</li>
))}
</ul>
)}
{query && results.length === 0 && (
<p style={{ padding: "10px 16px", color: "#9ca3af", margin: 0 }}>
No results found.
</p>
)}
</div>
);
}What this demonstrates:
- Global keyboard shortcut (
Cmd+K) usinguseEffectwithdocument.addEventListener - Local
onKeyDownhandler for arrow key navigation, Enter to select, and Escape to close - Tracking
selectedIndexstate for keyboard-driven list navigation - Combining React keyboard events with controlled input state
- Properly cleaning up global listeners in the
useEffectreturn function
Deep Dive
How It Works
- React keyboard events wrap the native
KeyboardEventin a Synthetic Event. The handler receives aReact.KeyboardEvent<T>with all the standard properties:key,code,altKey,ctrlKey,metaKey,shiftKey,repeat. onKeyDownfires when a key is pressed. It fires repeatedly if the key is held down (therepeatproperty istrueon subsequent fires).onKeyUpfires once when the key is released. Use it when you need the action only after the key is fully pressed and released.onKeyPressis deprecated and removed from the DOM spec. It does not fire for non-character keys (Escape, Arrow, Ctrl, etc.). Always useonKeyDowninstead.- Keyboard events only fire on focused elements. To capture global shortcuts, attach a listener to
documentinside auseEffect.
Variations
Single key handler with e.key:
function KeyLogger() {
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
console.log(`Key: ${e.key}, Code: ${e.code}, Repeat: ${e.repeat}`);
};
return (
<div tabIndex={0} onKeyDown={handleKeyDown}>
Focus me and press any key
</div>
);
}Modifier key combinations (Ctrl/Cmd+S to save):
function SaveHandler({ onSave }: { onSave: () => void }) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
const isMac = navigator.userAgent.includes("Mac");
const modifier = isMac ? e.metaKey : e.ctrlKey;
if (modifier && e.key === "s") {
e.preventDefault();
onSave();
}
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, [onSave]);
return null;
}Arrow key navigation in a list:
function ArrowNavList({ items }: { items: string[] }) {
const [activeIndex, setActiveIndex] = useState(0);
const handleKeyDown = (e: React.KeyboardEvent<HTMLUListElement>) => {
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setActiveIndex((i) => (i + 1) % items.length);
break;
case "ArrowUp":
e.preventDefault();
setActiveIndex((i) => (i - 1 + items.length) % items.length);
break;
case "Home":
e.preventDefault();
setActiveIndex(0);
break;
case "End":
e.preventDefault();
setActiveIndex(items.length - 1);
break;
}
};
return (
<ul tabIndex={0} onKeyDown={handleKeyDown} role="listbox">
{items.map((item, i) => (
<li
key={item}
role="option"
aria-selected={i === activeIndex}
style={{ background: i === activeIndex ? "#e0e7ff" : "transparent" }}
>
{item}
</li>
))}
</ul>
);
}Global keyboard shortcuts with a hook:
function useKeyboardShortcut(
key: string,
callback: () => void,
options: { ctrl?: boolean; meta?: boolean; shift?: boolean } = {}
) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (options.ctrl && !e.ctrlKey) return;
if (options.meta && !e.metaKey) return;
if (options.shift && !e.shiftKey) return;
if (e.key.toLowerCase() !== key.toLowerCase()) return;
e.preventDefault();
callback();
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, [key, callback, options.ctrl, options.meta, options.shift]);
}
// Usage
function App() {
useKeyboardShortcut("k", () => openSearch(), { meta: true });
useKeyboardShortcut("/", () => openSearch());
useKeyboardShortcut("Escape", () => closeModal());
return <div>...</div>;
}Preventing default browser shortcuts:
function PreventBrowserDefault() {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
// Prevent Ctrl+P (print) to open custom print dialog
if ((e.metaKey || e.ctrlKey) && e.key === "p") {
e.preventDefault();
console.log("Custom print dialog");
}
// Prevent Ctrl+F (find) to open custom search
if ((e.metaKey || e.ctrlKey) && e.key === "f") {
e.preventDefault();
console.log("Custom search");
}
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, []);
return <div>Browser shortcuts overridden</div>;
}TypeScript Notes
// React.KeyboardEvent<T> -- T is the element type
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
e.currentTarget; // HTMLInputElement
e.key; // "Enter", "Escape", "a", "ArrowDown", etc.
e.code; // "Enter", "Escape", "KeyA", "ArrowDown", etc.
};
// e.key vs e.code
// e.key: the character produced ("a", "A", "/", "Enter")
// -- affected by keyboard layout and modifier keys
// e.code: the physical key ("KeyA", "Slash", "Enter")
// -- consistent regardless of layout
// Modifier key properties (all boolean)
// e.altKey -- Alt (Option on Mac)
// e.ctrlKey -- Control
// e.metaKey -- Cmd on Mac, Windows key on Windows
// e.shiftKey -- Shift
// e.repeat -- true if key is held down
// Native KeyboardEvent (for useEffect listeners)
useEffect(() => {
const handler = (e: KeyboardEvent) => {
// This is the native DOM KeyboardEvent, NOT React.KeyboardEvent
// Same properties, but different type
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, []);
// Common pattern: typing a key handler map
type KeyHandlerMap = Record<string, (e: React.KeyboardEvent) => void>;
const handlers: KeyHandlerMap = {
Enter: (e) => submit(),
Escape: (e) => close(),
ArrowDown: (e) => { e.preventDefault(); moveDown(); },
ArrowUp: (e) => { e.preventDefault(); moveUp(); },
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
handlers[e.key]?.(e);
};Gotchas
-
onKeyPressis deprecated -- It does not fire for non-character keys (Escape, arrows, function keys, modifiers). It has been removed from the DOM spec. Fix: Always useonKeyDowninstead. The only reason to useonKeyUpis when you specifically need the action on key release. -
e.keyvalues differ frome.code--e.keyreturns"a"or"A"depending on Shift;e.codealways returns"KeyA". For keyboard shortcuts, usee.keywith.toLowerCase()to match regardless of case. For game controls or physical key position, usee.code. -
Keyboard events require focus -- A
<div>does not receive keyboard events unless it hastabIndex={0}(or-1for programmatic focus only). Fix: AddtabIndexto non-interactive elements that need keyboard handlers, or use globaldocument.addEventListenerfor shortcuts that should work regardless of focus. -
Global shortcuts leak across components -- If you register
Cmd+Kin auseEffectbut the component unmounts, the listener persists and fires on a stale closure. Fix: Always return a cleanup function fromuseEffectthat removes the listener. -
e.repeatfires continuously when a key is held -- Holding down a key firesonKeyDownrepeatedly. If your handler performs an expensive action (like an API call), it will fire dozens of times. Fix: Checkif (e.repeat) return;at the top of your handler if you only want the first press. -
Overriding browser shortcuts is unreliable -- Some browser shortcuts (like Ctrl+T, Ctrl+W, Ctrl+N) cannot be intercepted by JavaScript because the browser handles them before your code runs. Fix: Only override shortcuts you know are interceptable (Ctrl+S, Ctrl+P, Ctrl+F, etc.) and test across browsers.
-
Comparing
e.keywith string literals is fragile for special keys -- Thekeyvalues like"Enter","Escape","ArrowDown"are case-sensitive and follow the UI Events spec. Do not compare against"enter"or"esc". Fix: Use the exact spec values:"Enter","Escape","ArrowDown","ArrowUp","Tab", etc.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
accessKey HTML attribute | Simple one-key activation for buttons or links | You need modifier combinations or complex logic |
document.addEventListener in useEffect | Global shortcuts that work regardless of focused element | The shortcut is scoped to a specific input or component |
| Third-party libraries (react-hotkeys-hook, tinykeys) | Many shortcuts, chord sequences, or scope management | You have one or two simple shortcuts |
| ARIA keyboard patterns | Navigating composite widgets (menus, tabs, listboxes) | Simple form inputs that handle keys natively |
contentEditable | Rich text editing with full keyboard control | Standard form inputs or navigation |
FAQs
Which keyboard event should you use in React: onKeyDown, onKeyUp, or onKeyPress?
- Use
onKeyDownas your primary keyboard event -- it fires for all keys including non-character keys - Use
onKeyUponly when you need the action on key release - Never use
onKeyPress-- it is deprecated and does not fire for Escape, arrows, or modifier keys
What is the difference between e.key and e.code?
e.keyreturns the character produced ("a","A","Enter") and is affected by keyboard layout and modifierse.codereturns the physical key ("KeyA","Enter") and is consistent regardless of layout- Use
e.keyfor shortcuts; usee.codefor game controls or physical key position
How do you register a global keyboard shortcut like Cmd+K?
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
openSearch();
}
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, []);Why does a div not receive keyboard events by default?
Keyboard events only fire on focused elements. A <div> is not focusable by default. Add tabIndex={0} to make it keyboard-navigable, or tabIndex={-1} for programmatic focus only. Alternatively, use document.addEventListener for global shortcuts.
Gotcha: What happens if you hold down a key -- does onKeyDown fire once or repeatedly?
onKeyDown fires repeatedly when a key is held down. The e.repeat property is true on subsequent fires. If your handler does something expensive (like an API call), add if (e.repeat) return; at the top to only handle the first press.
How do you implement arrow key navigation in a list?
const handleKeyDown = (e: React.KeyboardEvent<HTMLUListElement>) => {
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setIndex((i) => (i + 1) % items.length);
break;
case "ArrowUp":
e.preventDefault();
setIndex((i) => (i - 1 + items.length) % items.length);
break;
}
};
return <ul tabIndex={0} onKeyDown={handleKeyDown} role="listbox">...</ul>;Gotcha: Why does my global shortcut listener fire after the component unmounts?
If you register a document.addEventListener in useEffect but forget the cleanup function, the listener persists after unmount and fires on a stale closure. Always return a cleanup function: return () => document.removeEventListener("keydown", handler);.
Can you intercept all browser keyboard shortcuts with e.preventDefault()?
No. Some shortcuts (Ctrl+T, Ctrl+W, Ctrl+N) are handled by the browser before your code runs and cannot be intercepted. Only override shortcuts you know are interceptable (Ctrl+S, Ctrl+P, Ctrl+F) and test across browsers.
How do you handle cross-platform modifier keys (Cmd on Mac, Ctrl on Windows)?
const handler = (e: KeyboardEvent) => {
const isMac = navigator.userAgent.includes("Mac");
const modifier = isMac ? e.metaKey : e.ctrlKey;
if (modifier && e.key === "s") {
e.preventDefault();
save();
}
};Are the e.key string values case-sensitive?
Yes. Key values like "Enter", "Escape", "ArrowDown" follow the UI Events spec and are case-sensitive. Comparing against "enter" or "esc" will not match. Always use the exact spec values.
What is the TypeScript type difference between React.KeyboardEvent and the native KeyboardEvent?
React.KeyboardEvent<T>is the synthetic event type used in JSX handlers (e.g.,onKeyDown)KeyboardEvent(no React prefix) is the native DOM type used inuseEffectwithdocument.addEventListener- They have the same properties but are different TypeScript types
How do you create a typed key handler map in TypeScript?
type KeyHandlerMap = Record<string, (e: React.KeyboardEvent) => void>;
const handlers: KeyHandlerMap = {
Enter: () => submit(),
Escape: () => close(),
ArrowDown: (e) => { e.preventDefault(); moveDown(); },
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
handlers[e.key]?.(e);
};Related
- Mouse Events -- Handling clicks, hovers, and mouse movement
- Form Events -- Handling form submissions and input changes