Skip to main content

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:

  • keydown fires when a key is pressed down. This is the primary event for most keyboard interactions.
  • keyup fires 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:

  1. keydown fires
  2. The character is inserted into the input (default action)
  3. keyup fires

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.

tip

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 Actionevent.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 Actionevent.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;
}
});
warning

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:

Keyevent.keyevent.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"
note

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:

PropertyKeyMac equivalent
event.shiftKeyShiftShift
event.ctrlKeyCtrlControl
event.altKeyAltOption (⌥)
event.metaKeyWindows keyCommand (⌘)
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();
}
});
tip

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);
note

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

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:

  1. Inconsistent behavior across browsers: Different browsers fire keypress for different keys. Some fire it for Escape, some do not. Some fire it for arrow keys, some do not.

  2. Does not fire for many important keys: Keys like Escape, Delete, Backspace, Arrow keys, and function keys may not trigger keypress at all, making it unreliable for any keyboard handling beyond basic text input.

  3. Unclear charCode/keyCode properties: The older event.charCode and event.keyCode properties used with keypress returned numeric values that varied between browsers and keyboard layouts. This made cross-browser code fragile and confusing.

  4. 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();
}
});
caution

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:

  • keydown fires when a key is pressed and is the primary event for most keyboard handling. Use it for shortcuts, input filtering, and game controls.
  • keyup fires when a key is released. Use it for detecting when a held key is let go.
  • event.key returns the logical character or key name (affected by keyboard layout and modifiers). Use it when you care about what character the user typed.
  • event.code returns 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. Check ctrlKey || metaKey for cross-platform shortcuts.
  • event.repeat identifies auto-repeated key events. Filter them out with !event.repeat for toggle actions.
  • event.preventDefault() on keydown stops default browser actions like character insertion, form submission, or page scrolling.
  • keypress is deprecated along with keyCode and charCode. Always use keydown/keyup with event.key or event.code in modern code.