How to Handle Keyboard Events in JavaScript
Keyboard interaction is fundamental to web applications. From simple form input to complex keyboard shortcuts, game controls, and accessibility features, understanding how the browser processes keyboard events gives you full control over user input. JavaScript provides two main keyboard events, keydown and keyup, each carrying detailed information about which key was pressed, whether modifier keys were held, and whether the key is being auto-repeated.
This guide covers everything you need to handle keyboard input confidently: the difference between event.key and event.code, how to detect modifier key combinations, how auto-repeat works, how to prevent default browser actions, and why the old keypress event should never be used in modern code.
keydown and keyup Events
JavaScript provides two keyboard events that cover the entire lifecycle of a key press:
keydownfires when a key is pressed down. This is the primary event for most keyboard interactions.keyupfires when a key is released.
document.addEventListener('keydown', (event) => {
console.log(`Key down: ${event.key}`);
});
document.addEventListener('keyup', (event) => {
console.log(`Key up: ${event.key}`);
});
Pressing and releasing the "A" key produces:
Key down: a
Key up: a
Event Order
When you press a key while focused on an input field, the full event sequence is:
keydownfires- The character is inserted into the input (default action)
keyupfires
This order matters because keydown fires before the character appears in the input, while keyup fires after:
<input id="demo" type="text" />
<script>
const input = document.getElementById('demo');
input.addEventListener('keydown', () => {
console.log('keydown - value:', input.value);
});
input.addEventListener('keyup', () => {
console.log('keyup - value:', input.value);
});
</script>
Typing "H" into an empty input:
keydown - value: // Empty! Character not inserted yet
keyup - value: H // Character is now in the input
This behavior is important when you need to read or validate the input value. On keydown, the value has not changed yet. On keyup, it has.
If you need to react to every change in an input's value in real time, the input event is usually a better choice than keyboard events. The input event fires after the value changes and works with paste, autofill, speech input, and other non-keyboard entry methods.
Where to Attach Keyboard Listeners
Keyboard events fire on the focused element. If no specific element has focus, they fire on document.body and bubble up to document.
// Listen for keyboard events anywhere on the page
document.addEventListener('keydown', (event) => {
console.log(`Global keydown: ${event.key}`);
});
// Listen for keyboard events on a specific input
const input = document.getElementById('search');
input.addEventListener('keydown', (event) => {
console.log(`Input keydown: ${event.key}`);
});
For global shortcuts (like Ctrl+S to save), attach the listener to document. For input-specific handling, attach to the input element itself.
event.key (Logical Key) vs. event.code (Physical Key)
Every keyboard event provides two properties that identify the pressed key. Understanding the difference between them is critical for writing correct keyboard handling code.
event.key: What the Key Produces
The key property returns the logical value of the key. It reflects the character that the key produces, taking into account the current keyboard layout, active language, and whether Shift or other modifiers are held.
document.addEventListener('keydown', (event) => {
console.log(`key: "${event.key}"`);
});
Pressing different keys:
| Physical Action | event.key |
|---|---|
| Press "A" | "a" |
| Press Shift + "A" | "A" |
| Press "1" | "1" |
| Press Shift + "1" (US layout) | "!" |
| Press Enter | "Enter" |
| Press Arrow Left | "ArrowLeft" |
| Press Escape | "Escape" |
| Press Shift (alone) | "Shift" |
For printable characters, event.key is the character itself. For special keys, it is a descriptive string like "Enter", "Escape", "ArrowUp", "Tab", "Backspace", "Delete", "Home", "End", etc.
event.code: Where the Key Is on the Keyboard
The code property returns the physical key code, identifying the key by its position on the keyboard. It does not change with keyboard layout or language.
document.addEventListener('keydown', (event) => {
console.log(`code: "${event.code}"`);
});
| Physical Action | event.code |
|---|---|
| Press "A" key | "KeyA" |
| Press Shift + "A" key | "KeyA" |
| Press "1" key (top row) | "Digit1" |
| Press "1" key (numpad) | "Numpad1" |
| Press Enter | "Enter" |
| Press Left Shift | "ShiftLeft" |
| Press Right Shift | "ShiftRight" |
| Press Arrow Left | "ArrowLeft" |
Notice that event.code is always the same for a physical key, regardless of Shift state or keyboard layout.
When Layout Matters: key vs. code
The difference becomes critical with non-US keyboard layouts. On a QWERTY keyboard, the key in the top-left letter row is "Q". On a AZERTY keyboard (French), the same physical key is "A". On a QWERTZ keyboard (German), the "Z" and "Y" keys are swapped.
// User with a French AZERTY keyboard presses the key
// in the top-left letter position:
// event.key = "a" (the character it produces in French)
// event.code = "KeyQ" (the physical position, same as QWERTY Q)
Which One Should You Use?
The choice depends on what you are building:
Use event.key when you care about the character:
- Text input and filtering
- Search functionality
- Any situation where the typed character matters
// Filter input to allow only digits
input.addEventListener('keydown', (event) => {
if (event.key.length === 1 && !/\d/.test(event.key)) {
event.preventDefault(); // Block non-digit characters
}
});
Use event.code when you care about the physical position:
- Game controls (WASD movement)
- Keyboard shortcuts that should work regardless of layout
- Piano or musical instrument apps
// Game movement: works on any keyboard layout
document.addEventListener('keydown', (event) => {
switch (event.code) {
case 'KeyW': moveUp(); break; // Always top-left cluster
case 'KeyA': moveLeft(); break;
case 'KeyS': moveDown(); break;
case 'KeyD': moveRight(); break;
case 'Space': jump(); break;
}
});
If you use event.key for game controls, players with non-QWERTY keyboards will have keys in wrong positions. If you use event.code for text shortcuts like "Ctrl+C for copy", some layouts may have "C" on a different physical key. Choose based on your use case.
Common event.key and event.code Values
Here is a reference table for frequently used keys:
| Key | event.key | event.code |
|---|---|---|
| Letters | "a" to "z" (or uppercase with Shift) | "KeyA" to "KeyZ" |
| Digits (top row) | "0" to "9" | "Digit0" to "Digit9" |
| Digits (numpad) | "0" to "9" | "Numpad0" to "Numpad9" |
| Enter | "Enter" | "Enter" or "NumpadEnter" |
| Space | " " | "Space" |
| Tab | "Tab" | "Tab" |
| Escape | "Escape" | "Escape" |
| Backspace | "Backspace" | "Backspace" |
| Delete | "Delete" | "Delete" |
| Arrow keys | "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight" | Same |
| Shift | "Shift" | "ShiftLeft" or "ShiftRight" |
| Control | "Control" | "ControlLeft" or "ControlRight" |
| Alt | "Alt" | "AltLeft" or "AltRight" |
| Meta (Cmd/Win) | "Meta" | "MetaLeft" or "MetaRight" |
| F1-F12 | "F1" to "F12" | "F1" to "F12" |
Notice that event.code distinguishes between left and right modifier keys ("ShiftLeft" vs. "ShiftRight") while event.key does not ("Shift" for both). Also, event.code distinguishes between the main Enter key ("Enter") and the numpad Enter ("NumpadEnter"), while event.key returns "Enter" for both.
Modifier Keys
Modifier keys (Shift, Ctrl, Alt, Meta) are rarely used alone. They modify the behavior of other keys. Keyboard events provide boolean properties to check whether any modifier was held during the key press:
| Property | Key | Mac equivalent |
|---|---|---|
event.shiftKey | Shift | Shift |
event.ctrlKey | Ctrl | Control |
event.altKey | Alt | Option (⌥) |
event.metaKey | Windows key | Command (⌘) |
document.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.key === 's') {
event.preventDefault(); // Prevent browser's Save dialog
saveDocument();
console.log('Document saved!');
}
});
Handling Cross-Platform Shortcuts
On macOS, users expect Command (⌘) as the primary modifier, while Windows and Linux users expect Ctrl. The metaKey property detects the Command key:
document.addEventListener('keydown', (event) => {
// Ctrl on Windows/Linux, Cmd on macOS
const isModifier = event.ctrlKey || event.metaKey;
if (isModifier && event.key === 's') {
event.preventDefault();
saveDocument();
}
if (isModifier && event.key === 'z') {
event.preventDefault();
undo();
}
if (isModifier && event.shiftKey && event.key === 'z') {
event.preventDefault();
redo();
}
});
For shortcuts that should work on both macOS and Windows, check event.ctrlKey || event.metaKey. This covers Ctrl+S on Windows and Cmd+S on macOS with a single condition.
Detecting Multiple Modifiers
You can combine modifier checks for complex shortcuts:
document.addEventListener('keydown', (event) => {
// Ctrl+Shift+P (or Cmd+Shift+P on Mac)
if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.code === 'KeyP') {
event.preventDefault();
openCommandPalette();
}
// Alt+Enter
if (event.altKey && event.key === 'Enter') {
event.preventDefault();
submitAndContinue();
}
});
Modifier Keys Fire Their Own Events
Pressing a modifier key by itself triggers keydown and keyup events. This is important if you want to show a UI hint when the user holds a modifier:
document.addEventListener('keydown', (event) => {
if (event.key === 'Alt') {
showMenuShortcuts(); // Show underlined shortcut letters in menus
}
});
document.addEventListener('keyup', (event) => {
if (event.key === 'Alt') {
hideMenuShortcuts();
}
});
A Reusable Shortcut Handler
For applications with many keyboard shortcuts, a structured approach is cleaner:
class KeyboardShortcuts {
constructor() {
this.shortcuts = [];
document.addEventListener('keydown', (e) => this.handle(e));
}
add(options, callback) {
this.shortcuts.push({ ...options, callback });
}
handle(event) {
for (const shortcut of this.shortcuts) {
const modifierMatch =
(shortcut.ctrl ?? false) === (event.ctrlKey || event.metaKey) &&
(shortcut.shift ?? false) === event.shiftKey &&
(shortcut.alt ?? false) === event.altKey;
const keyMatch = shortcut.key
? event.key.toLowerCase() === shortcut.key.toLowerCase()
: shortcut.code
? event.code === shortcut.code
: false;
if (modifierMatch && keyMatch) {
event.preventDefault();
shortcut.callback(event);
return;
}
}
}
}
// Usage
const shortcuts = new KeyboardShortcuts();
shortcuts.add({ ctrl: true, key: 's' }, () => {
console.log('Save');
});
shortcuts.add({ ctrl: true, shift: true, key: 'p' }, () => {
console.log('Command palette');
});
shortcuts.add({ key: 'Escape' }, () => {
console.log('Close dialog');
});
shortcuts.add({ ctrl: true, key: 'k' }, () => {
console.log('Open search');
});
Auto-Repeat (event.repeat)
When a user holds down a key, the operating system generates repeated key presses after a short delay. Each of these repeated presses fires another keydown event. The event.repeat property tells you whether the event was generated by auto-repeat.
document.addEventListener('keydown', (event) => {
if (event.repeat) {
console.log(`Repeating: ${event.key}`);
} else {
console.log(`First press: ${event.key}`);
}
});
Holding down the "A" key produces:
First press: a
Repeating: a
Repeating: a
Repeating: a
... (continues until key is released)
Only one keyup event fires when the key is finally released.
Ignoring Auto-Repeat
For many interactions, you want to respond only to the initial press and ignore repeats. For example, a "toggle fullscreen" shortcut should toggle once, not rapidly flicker:
// ❌ Toggles rapidly when key is held
document.addEventListener('keydown', (event) => {
if (event.key === 'f') {
toggleFullscreen();
}
});
// ✅ Toggles only on the initial press
document.addEventListener('keydown', (event) => {
if (event.key === 'f' && !event.repeat) {
toggleFullscreen();
}
});
Using Auto-Repeat Intentionally
Sometimes auto-repeat is desirable. Scrolling or moving an object while holding an arrow key should continue as long as the key is held:
document.addEventListener('keydown', (event) => {
// Auto-repeat is fine here: continuous movement
switch (event.code) {
case 'ArrowUp':
scrollContent(-20);
break;
case 'ArrowDown':
scrollContent(20);
break;
}
});
Tracking Held Keys
For game-like interactions where you need to know which keys are currently held, track them manually with a Set:
const heldKeys = new Set();
document.addEventListener('keydown', (event) => {
heldKeys.add(event.code);
});
document.addEventListener('keyup', (event) => {
heldKeys.delete(event.code);
});
// In your game loop or animation frame:
function gameLoop() {
if (heldKeys.has('KeyW')) moveForward();
if (heldKeys.has('KeyA')) moveLeft();
if (heldKeys.has('KeyS')) moveBackward();
if (heldKeys.has('KeyD')) moveRight();
if (heldKeys.has('Space') && heldKeys.has('ShiftLeft')) {
sprintJump(); // Multiple keys held simultaneously
}
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
Using a Set to track held keys is more reliable than relying on auto-repeat for game input. Auto-repeat has an initial delay and its rate depends on OS settings, while the Set approach gives you frame-perfect tracking.
Edge Case: Lost keyup Events
There is a subtle issue with key tracking: if the user switches browser tabs or windows while holding a key, the keyup event fires on the other window and your code never receives it. The key remains "stuck" in your heldKeys set.
To handle this, clear the set when the window loses focus:
window.addEventListener('blur', () => {
heldKeys.clear(); // Release all "stuck" keys
});
Default Actions: Preventing Input and Shortcuts
Many keys have built-in browser actions. Pressing a character key in an input types that character. Pressing Tab moves focus. Pressing Ctrl+P opens the print dialog. You can prevent these defaults with event.preventDefault() on the keydown event.
Restricting Input Characters
A common use case is restricting an input to only accept certain characters:
<label>Phone number: <input id="phone" type="text" placeholder="Digits only" /></label>
<script>
const phoneInput = document.getElementById('phone');
phoneInput.addEventListener('keydown', (event) => {
// Allow: digits, backspace, delete, tab, arrows, home, end
const allowedKeys = [
'Backspace', 'Delete', 'Tab', 'Escape',
'ArrowLeft', 'ArrowRight', 'Home', 'End'
];
if (allowedKeys.includes(event.key)) return; // Allow these keys
// Allow Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X
if ((event.ctrlKey || event.metaKey) && ['a', 'c', 'v', 'x'].includes(event.key)) {
return;
}
// Allow digits
if (/^\d$/.test(event.key)) return;
// Block everything else
event.preventDefault();
});
</script>
Keyboard-based input filtering has limitations. Users can paste non-matching text using the right-click context menu or browser autofill. Always validate input on the server side and consider using the input event for client-side validation instead:
phoneInput.addEventListener('input', () => {
phoneInput.value = phoneInput.value.replace(/\D/g, '');
});
Preventing Browser Shortcuts
You can override certain browser shortcuts, but not all. The browser allows preventing shortcuts that do not involve critical security functions:
document.addEventListener('keydown', (event) => {
// Override Ctrl+S (Save page)
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault();
saveToServer();
}
// Override Ctrl+P (Print)
if ((event.ctrlKey || event.metaKey) && event.key === 'p') {
event.preventDefault();
openCustomPrintDialog();
}
});
Some shortcuts cannot be overridden because the browser intercepts them before your JavaScript runs. Examples include:
- Ctrl+T (new tab) in most browsers
- Ctrl+W (close tab) in most browsers
- Ctrl+N (new window) in most browsers
- F11 (fullscreen) in some browsers
- Alt+F4 (close window) on Windows
The exact set of non-overridable shortcuts varies between browsers and operating systems.
Preventing Default on Special Keys
Some specific default actions you might want to prevent:
// Prevent Tab from moving focus (e.g., in a code editor)
editor.addEventListener('keydown', (event) => {
if (event.key === 'Tab') {
event.preventDefault();
insertAtCursor('\t'); // Insert a tab character instead
}
});
// Prevent Enter from submitting a form
form.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && event.target.tagName !== 'TEXTAREA') {
event.preventDefault();
}
});
// Prevent Space from scrolling the page (e.g., in a game)
document.addEventListener('keydown', (event) => {
if (event.code === 'Space' && event.target === document.body) {
event.preventDefault();
}
});
// Prevent Backspace from navigating back (legacy browser behavior)
document.addEventListener('keydown', (event) => {
if (event.key === 'Backspace' && !isInputElement(event.target)) {
event.preventDefault();
}
});
function isInputElement(el) {
const tag = el.tagName;
return tag === 'INPUT' || tag === 'TEXTAREA' || el.isContentEditable;
}
keydown vs. keyup for Preventing Defaults
Always prevent defaults on keydown, not keyup. The default action (character insertion, form submission, etc.) happens between keydown and keyup. By the time keyup fires, the action has already occurred:
// ❌ Too late: the character is already in the input
input.addEventListener('keyup', (event) => {
event.preventDefault(); // Does nothing useful
});
// ✅ Prevents the character from being inserted
input.addEventListener('keydown', (event) => {
event.preventDefault();
});
The Deprecated keypress Event
The keypress event is an older keyboard event that was used to detect character input. It was designed to fire only for keys that produce a character (letters, digits, punctuation) and not for special keys (arrows, Escape, function keys).
// ❌ Deprecated: do not use in new code
document.addEventListener('keypress', (event) => {
console.log(`keypress: ${event.key}`);
});
Why keypress Is Deprecated
The keypress event has several problems:
-
Inconsistent behavior across browsers: Different browsers fire
keypressfor different keys. Some fire it for Escape, some do not. Some fire it for arrow keys, some do not. -
Does not fire for many important keys: Keys like Escape, Delete, Backspace, Arrow keys, and function keys may not trigger
keypressat all, making it unreliable for any keyboard handling beyond basic text input. -
Unclear
charCode/keyCodeproperties: The olderevent.charCodeandevent.keyCodeproperties used withkeypressreturned numeric values that varied between browsers and keyboard layouts. This made cross-browser code fragile and confusing. -
Officially removed from the specification: The W3C and WHATWG standards have deprecated
keypress. It still works in browsers for backward compatibility, but it may be removed in the future.
What to Use Instead
Replace keypress with keydown for all keyboard handling:
// ❌ Old way
element.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
submitForm();
}
});
// ✅ Modern way
element.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
submitForm();
}
});
For detecting character input specifically, the input event is a better choice than either keypress or keydown:
// ✅ Best for reacting to text input changes
inputElement.addEventListener('input', (event) => {
console.log('New value:', inputElement.value);
console.log('Data inserted:', event.data); // The inserted character(s)
});
Avoid keyCode and charCode
Along with keypress, the numeric event.keyCode and event.charCode properties are deprecated. They return browser-specific numeric codes that are not standardized:
// ❌ Deprecated: numeric codes are unreliable
document.addEventListener('keydown', (event) => {
if (event.keyCode === 13) { // 13 = Enter... on most browsers
submitForm();
}
});
// ✅ Modern: string-based, clear and standardized
document.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
submitForm();
}
});
If you encounter keyCode, charCode, or which in existing code, consider migrating to event.key and event.code. The numeric properties still work in browsers, but they are not standardized, produce different values across browsers and layouts, and make code harder to read and maintain.
Practical Example: Accessible Keyboard Navigation
Here is a complete example that combines several concepts covered in this guide to build a keyboard-navigable component:
<style>
.list-item {
padding: 10px 16px;
cursor: pointer;
border: 2px solid transparent;
border-radius: 6px;
margin: 4px 0;
background: #f5f5f5;
transition: all 0.15s;
}
.list-item:hover {
background: #e8e8e8;
}
.list-item.focused {
border-color: #3498db;
background: #ebf5fb;
}
.list-item.selected {
background: #3498db;
color: white;
}
</style>
<div id="item-list" tabindex="0" role="listbox" aria-label="Select an item">
<div class="list-item" role="option">Apple</div>
<div class="list-item" role="option">Banana</div>
<div class="list-item" role="option">Cherry</div>
<div class="list-item" role="option">Date</div>
<div class="list-item" role="option">Elderberry</div>
</div>
<script>
const list = document.getElementById('item-list');
const items = list.querySelectorAll('.list-item');
let focusedIndex = -1;
function setFocusedIndex(index) {
// Remove previous focus
if (focusedIndex >= 0 && focusedIndex < items.length) {
items[focusedIndex].classList.remove('focused');
}
// Clamp index within bounds
focusedIndex = Math.max(0, Math.min(index, items.length - 1));
// Apply new focus
items[focusedIndex].classList.add('focused');
items[focusedIndex].scrollIntoView({ block: 'nearest' });
}
function selectItem(index) {
items.forEach(item => item.classList.remove('selected'));
items[index].classList.add('selected');
console.log(`Selected: ${items[index].textContent}`);
}
list.addEventListener('keydown', (event) => {
switch (event.key) {
case 'ArrowDown':
event.preventDefault(); // Prevent page scroll
setFocusedIndex(focusedIndex + 1);
break;
case 'ArrowUp':
event.preventDefault();
setFocusedIndex(focusedIndex - 1);
break;
case 'Home':
event.preventDefault();
setFocusedIndex(0);
break;
case 'End':
event.preventDefault();
setFocusedIndex(items.length - 1);
break;
case 'Enter':
case ' ':
event.preventDefault();
if (focusedIndex >= 0) {
selectItem(focusedIndex);
}
break;
}
});
// Also support mouse clicks
items.forEach((item, index) => {
item.addEventListener('click', () => {
setFocusedIndex(index);
selectItem(index);
});
});
</script>
This component uses event.key for readable key comparisons, prevents default scrolling behavior for arrow keys, supports Home and End navigation, and works alongside mouse click interaction.
Summary
Keyboard events in JavaScript are built around two core events and two key identification properties:
keydownfires when a key is pressed and is the primary event for most keyboard handling. Use it for shortcuts, input filtering, and game controls.keyupfires when a key is released. Use it for detecting when a held key is let go.event.keyreturns the logical character or key name (affected by keyboard layout and modifiers). Use it when you care about what character the user typed.event.codereturns the physical key position on the keyboard (unaffected by layout). Use it for game controls and layout-independent shortcuts.- Modifier properties (
ctrlKey,shiftKey,altKey,metaKey) let you detect key combinations. CheckctrlKey || metaKeyfor cross-platform shortcuts. event.repeatidentifies auto-repeated key events. Filter them out with!event.repeatfor toggle actions.event.preventDefault()onkeydownstops default browser actions like character insertion, form submission, or page scrolling.keypressis deprecated along withkeyCodeandcharCode. Always usekeydown/keyupwithevent.keyorevent.codein modern code.