ToolPal
Close-up of a computer keyboard

JavaScript Keyboard Events Explained: key, code, keyCode, and How to Debug Them

πŸ“· Life Of Pix / Pexels

JavaScript Keyboard Events Explained: key, code, keyCode, and How to Debug Them

A practical guide to understanding keyboard events in JavaScript β€” why keyCode is deprecated, how key and code differ, and how to build reliable keyboard shortcuts that work across browsers and layouts.

April 9, 202612 min read

Here's a bug I've hit more than once: I write a keyboard shortcut handler in Chrome, everything works perfectly, I ship it, and then I get a bug report from someone on Firefox. Same code, different behavior. After an hour of digging, I find the culprit β€” I was using e.keyCode to detect the key, and it was returning different values depending on the browser and the user's keyboard layout.

That bug is entirely avoidable, and it's why understanding JavaScript keyboard events properly is worth your time. This post walks through the full keyboard event model β€” which events fire, what properties to use, which ones to avoid, and how to build shortcuts that actually work everywhere.

The Three Events: keydown, keypress, keyup

When a user presses a key, the browser fires up to three events in sequence:

  1. keydown β€” fires immediately when the key is pressed, before any character is inserted
  2. keypress β€” fires after keydown, but only for keys that produce a character value (letters, numbers, punctuation)
  3. keyup β€” fires when the key is released

The short version: use keydown for most things. Here's why:

keypress is deprecated. It never fired for non-printing keys like Escape, Delete, F1-F12, or the arrow keys. If your shortcut handler only worked for letter keys, keypress was probably the reason. Don't use it for new code.

keyup is useful when you specifically want to react after the key is released β€” like updating a preview after the user finishes typing a character. But for shortcuts and game controls, keydown gives you faster, more predictable behavior.

keydown also has the advantage that it fires repeatedly when a key is held down (at the OS key repeat rate), which is exactly what you want for things like scrolling or moving a game character.

document.addEventListener('keydown', (e) => {
  // This is where you want to be
  console.log(e.key, e.code);
});

The Properties That Actually Matter

When a KeyboardEvent fires, the event object has a bunch of properties. Let me go through the ones you need to know.

key β€” What the key produces

key gives you the string value of what the key represents in the current context. For a regular letter key, it's the character: "a", "A" (with Shift), "Γ©" (on a French keyboard). For special keys, it's a descriptive name: "Enter", "Escape", "ArrowLeft", "F5", "Backspace".

This is the property you should reach for first. It's layout-aware, modifier-aware, and tells you what the key means to the user.

document.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    submitForm();
  }
  if (e.key === 'Escape') {
    closeModal();
  }
  if (e.key === 'ArrowLeft') {
    goToPreviousSlide();
  }
});

One gotcha: key is case-sensitive and modifier-sensitive. Pressing the a key without Shift gives "a". With Shift, it gives "A". So if you're checking for a shortcut that shouldn't depend on case, normalize it:

if (e.key.toLowerCase() === 'k') {
  openCommandPalette();
}

code β€” Which physical key was pressed

code gives you the identifier for the physical key position on the keyboard, regardless of what modifier keys are held or what keyboard layout the user has. Pressing the key in the top-left of the letter area is always "KeyQ" β€” even if the user's keyboard layout puts "A" there (like on an AZERTY layout).

The format is consistent: letter keys are "KeyA" through "KeyZ", digit keys are "Digit0" through "Digit9", function keys are "F1" through "F12", and so on.

Use code when you care about the physical location of the key rather than what it produces. The classic use case is game controls:

document.addEventListener('keydown', (e) => {
  switch (e.code) {
    case 'KeyW':
    case 'ArrowUp':
      player.moveUp();
      break;
    case 'KeyS':
    case 'ArrowDown':
      player.moveDown();
      break;
    case 'KeyA':
    case 'ArrowLeft':
      player.moveLeft();
      break;
    case 'KeyD':
    case 'ArrowRight':
      player.moveRight();
      break;
  }
});

Using code here means WASD controls work the same way regardless of whether the player has a QWERTY, AZERTY, or Dvorak keyboard. On AZERTY, the W key is in a different position, so e.key would give you "z" β€” but e.code still gives you "KeyW" because it's the same physical position.

keyCode and which β€” Legacy, deprecated, avoid them

keyCode and which are the old way. They give you a numeric code for the key β€” 65 for A, 13 for Enter, 27 for Escape. The problem is these values weren't consistently defined, especially for punctuation and special keys. Different browsers returned different numbers, and the values were based on Windows virtual key codes in ways that made zero intuitive sense.

The MDN docs mark both keyCode and which as deprecated. They're still around for backward compatibility, but you should not use them in new code. If you're maintaining code that uses them, plan to migrate.

Old code you might see:

// Don't do this
if (e.keyCode === 13) { /* Enter */ }
if (e.which === 27) { /* Escape */ }

Modern equivalent:

// Do this instead
if (e.key === 'Enter') { /* Enter */ }
if (e.key === 'Escape') { /* Escape */ }

charCode β€” Only in keypress, also deprecated

charCode was the Unicode code point of the character, but it only worked in keypress events, and only for printable characters. Since keypress itself is deprecated, charCode is doubly deprecated. Just use e.key and call .codePointAt(0) if you need the Unicode value.

location β€” For keys that appear in multiple places

location tells you which instance of a key was pressed, for keys that appear in multiple places on the keyboard. It's a number: 0 is the standard position, 1 is the left modifier, 2 is the right modifier, and 3 is the numpad.

So e.location === 1 with e.key === "Shift" means the left Shift key was pressed. e.location === 3 with e.key === "5" means the numpad 5 was pressed. It's rarely needed but good to know it exists.

Modifier Keys: Ctrl, Shift, Alt, Meta

For keyboard shortcuts, you usually combine a regular key with one or more modifier keys. The event object has boolean properties for each modifier:

  • e.ctrlKey β€” Ctrl is held down
  • e.shiftKey β€” Shift is held down
  • e.altKey β€” Alt (or Option on Mac) is held down
  • e.metaKey β€” Meta is held down (Command/Cmd on Mac, Windows key on Windows)

These are always set correctly for the current event, regardless of which property you use to identify the key. You just combine them:

document.addEventListener('keydown', (e) => {
  // Ctrl+S (or Cmd+S on Mac)
  if ((e.ctrlKey || e.metaKey) && e.key === 's') {
    e.preventDefault(); // Don't trigger browser's Save dialog
    saveDocument();
  }

  // Ctrl+Shift+P β€” command palette
  if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'p') {
    e.preventDefault();
    openCommandPalette();
  }

  // Alt+Left β€” go back
  if (e.altKey && e.key === 'ArrowLeft') {
    goBack();
  }
});

Note the e.ctrlKey || e.metaKey pattern β€” this is the standard cross-platform shortcut approach. On macOS, most shortcuts use Command (metaKey). On Windows/Linux, they use Ctrl. This pattern handles both.

Also notice e.preventDefault(). If your shortcut conflicts with a browser shortcut, you need to call this to stop the browser from acting on it. Ctrl+S would open the browser's Save dialog without it.

The International Keyboard Problem

Here's a scenario that trips up a lot of developers: your app has a keyboard shortcut, say Ctrl+/ for toggling comments. You test it on your en-US keyboard, works great. A user in Germany files a bug report β€” the shortcut doesn't work.

On a German keyboard layout, the / character isn't on a dedicated key. It's accessed via Shift+7 or a different combination. So e.key when pressing that key position is completely different from what you'd expect.

Two approaches to handle this:

Option 1: Use e.code for position-based shortcuts.

// The / key on a US keyboard is in a specific physical position
// code will always be 'Slash' for that position
if (e.ctrlKey && e.code === 'Slash') {
  toggleComment();
}

This works if the shortcut makes sense based on key position. But it can feel unintuitive to users on other layouts β€” they're pressing a key that says something completely different from what they expect.

Option 2: Let users configure their own shortcuts.

Apps like VS Code do this well β€” they let you rebind any shortcut to whatever key combination works for you. More work to implement, but the right answer for a professional tool with an international user base.

Option 3: Accept multiple key values.

// Accept both the US layout value and common alternatives
const toggleKeys = new Set(['/', '?', '-']);
if (e.ctrlKey && toggleKeys.has(e.key)) {
  toggleComment();
}

Not great, but sometimes pragmatic for simple cases.

Building a Robust Shortcut Handler

For any non-trivial application, it's worth building a centralized shortcut system rather than scattering addEventListener calls everywhere. Here's a pattern that works well:

const shortcuts = {
  'ctrl+s': () => saveDocument(),
  'ctrl+shift+p': () => openCommandPalette(),
  'ctrl+z': () => undo(),
  'ctrl+shift+z': () => redo(),
  'escape': () => closeModal(),
  '?': () => showHelp(),
};

function getShortcutKey(e) {
  const parts = [];
  if (e.ctrlKey || e.metaKey) parts.push('ctrl');
  if (e.shiftKey) parts.push('shift');
  if (e.altKey) parts.push('alt');
  parts.push(e.key.toLowerCase());
  return parts.join('+');
}

document.addEventListener('keydown', (e) => {
  // Don't fire shortcuts when user is typing in an input
  if (e.target.matches('input, textarea, select, [contenteditable]')) {
    // Maybe still allow Escape
    if (e.key !== 'Escape') return;
  }

  const shortcutKey = getShortcutKey(e);
  const handler = shortcuts[shortcutKey];
  if (handler) {
    e.preventDefault();
    handler();
  }
});

The e.target.matches(...) check is important β€” you almost never want your app-level shortcuts to fire when the user is actively typing in a form field. Escape is a common exception because it's used to dismiss modals and cancel operations.

What You Can't Intercept

Not all key combinations are available to JavaScript. Some are claimed by the browser or the operating system, and your code never sees the event at all:

  • Ctrl+W / Cmd+W β€” close the current tab
  • Ctrl+T / Cmd+T β€” open a new tab
  • Ctrl+N / Cmd+N β€” new window
  • Ctrl+Tab / Cmd+Tab β€” switch tabs/apps
  • Cmd+Space (macOS) β€” Spotlight
  • Alt+F4 (Windows) β€” close window
  • F5 / Ctrl+R β€” page refresh (you can override F5, but not always Ctrl+R)
  • PrintScreen β€” screenshot on Windows

This catches people out when they try to build things like "prevent the user from leaving the page" or "intercept Ctrl+W to show a warning." You can use the beforeunload event for navigation warnings, but you can't intercept the keyboard shortcut itself.

Accessibility: Keyboard Navigation Matters

If you're adding custom keyboard interactions to your app, don't forget that keyboard navigation is a core accessibility requirement. Users who rely on screen readers or can't use a mouse depend on keyboard access.

A few things to keep in mind:

Focus management. Make sure interactive elements are focusable (tabindex="0" if needed) and that focus moves logically as the UI changes. When you open a modal, move focus inside it. When you close it, move focus back to the element that triggered it.

Don't trap focus (unless it's a modal). Users should be able to Tab through your interface without getting stuck.

Don't rely solely on keyboard shortcuts that users might not discover. Provide visible UI alternatives for every action.

Test with a screen reader. VoiceOver on macOS, NVDA or JAWS on Windows. Keyboard events interact with screen readers in ways you won't discover from code review alone.

Debugging With the Keycode Viewer

When you're actually building keyboard interactions, the hardest part is often just figuring out what values the browser is giving you for a specific key press. Documentation is great, but sometimes you just need to press the key and see the output.

That's exactly what our Keycode Viewer tool does. Open it, press any key, and it immediately shows you:

  • key β€” the logical value
  • code β€” the physical position identifier
  • keyCode β€” the legacy numeric value (for reference when working with older code)
  • which β€” the other legacy property
  • charCode β€” character code from keypress events
  • location β€” standard/left/right/numpad
  • ctrlKey, shiftKey, altKey, metaKey β€” modifier states

It's especially useful when you're dealing with international keyboards, special characters, or trying to figure out why your existing code isn't recognizing a particular key. Instead of adding console.log statements and reloading, you just open the tool and press the key.

I find it most useful for two scenarios: when I'm building a shortcut and want to verify the exact code or key value before hardcoding it, and when I'm debugging someone else's keyboard handler and need to know what values are actually coming through.

Quick Reference

PropertyUse when you wantDeprecated?
keyThe character or action nameNo
codeThe physical key positionNo
keyCodeNothing β€” legacy onlyYes
whichNothing β€” legacy onlyYes
charCodeNothing β€” legacy onlyYes
ctrlKeyCheck if Ctrl is heldNo
shiftKeyCheck if Shift is heldNo
altKeyCheck if Alt/Option is heldNo
metaKeyCheck if Cmd/Win key is heldNo

The main mental model: reach for e.key first. It tells you what the key means. Only use e.code when you specifically care about physical position (game controls, layout-independent shortcuts). Never use keyCode, which, or charCode in new code.

Get this right and your keyboard interactions will be consistent across browsers, keyboard layouts, and operating systems. And you'll stop getting those bug reports from Firefox users.

Frequently Asked Questions

Share this article

XLinkedIn

Related Posts