DevToys Web Pro iconDevToys Web ProBlog
Traduït amb LocalePack logoLocalePack
Valora'ns:
Prova l'extensió del navegador:
← Back to Blog

JavaScript Keyboard Events: event.key vs event.code vs keyCode

8 min read

Keyboard handling in JavaScript has three distinct generations of APIs, and picking the wrong one still causes bugs in 2026 — especially for users on non-US keyboard layouts or macOS. Before writing any shortcut handler, open the Keyboard Event Tester and press a few keys. You will immediately see the difference between key, code, and the legacy keyCode on your own machine.

Three Generations of Keyboard APIs

The browser keyboard API evolved through three layers, each introduced to fix problems in the previous one:

  • DOM Level 0 — event.which: A Netscape-era property that returned different values in different browsers. Never standardized. Avoid entirely.
  • DOM Level 3 (legacy) — event.keyCode / event.charCode: Numeric codes standardized by Internet Explorer and carried into the spec for compatibility. keyCode was officially marked deprecated in the DOM Level 3 spec. All modern browsers still fire it, but you should not write new code against it.
  • DOM Level 3 (current) — event.key / event.code: Two separate string-based properties introduced to cleanly separate the logical character from the physical key position. These are what you should use.

Comparison: What Each Property Returns

The table below shows what each property returns when a user presses Shift+A on three different keyboard layouts:

PropertyQWERTY (Shift+A)AZERTY (Shift+Q)Dvorak (Shift+A)
event.keyCode656565
event.key"A""A""A"
event.code"KeyA""KeyQ""KeyA"

On AZERTY, the physical key that sits where A is on QWERTY is labeled Q. So event.code reports "KeyQ" (the physical position), while event.key still reports "A" (the character produced after Shift is applied). The numeric keyCode happens to agree here — 65 — but breaks badly on special characters and non-Latin layouts.

event.code — Physical Key Location

event.code identifies the physical key on the keyboard, completely independent of the active layout. The value is a string like "KeyA", "ArrowLeft", "Space", or "Digit1". Because it maps to position, not character, it is the right choice for:

  • Game controls — WASD movement works correctly even when the player switches to a French AZERTY layout, where those letters sit in different positions. Using event.key for WASD would break immediately.
  • Hotkeys that should not shift with layout — shortcuts like Ctrl+Z, Ctrl+S where the letter is chosen for muscle memory, not the character.
// Game movement — use event.code, not event.key
window.addEventListener('keydown', (e) => {
  switch (e.code) {
    case 'KeyW': moveForward(); break;
    case 'KeyA': moveLeft();    break;
    case 'KeyS': moveBack();    break;
    case 'KeyD': moveRight();   break;
  }
});

event.key — The Character Produced

event.key returns the string value of the key after the active modifier state is applied. For printable characters this is the character itself ("a", "A", "€"). For non-printable keys it is a named value: "Enter", "Backspace", "ArrowUp", "Escape".

event.key is the right choice when you care about the character the user intends to produce — text input validation, autocomplete trigger keys, or shortcuts that should follow the user's layout. It is also IME-aware: during composition (typing CJK characters with an input method editor), event.key returns "Process", which lets you skip shortcut logic during composition.

Deprecated Numeric Codes — Why They Still Appear

Despite being deprecated, event.keyCode is still fired by every browser and is unlikely to be removed. The reasons you still encounter it:

  • Legacy codebases pre-dating 2016 DOM Level 3 adoption use numeric codes throughout.
  • Some internal enterprise tooling was frozen at IE11 compatibility. IE11 does not implement event.code at all, so the only cross-IE option was keyCode.
  • Tutorials and Stack Overflow answers from before 2016 proliferate numeric constants without deprecation warnings.

If you genuinely need to support IE11 (rare in 2026, but still common in regulated industries), you can polyfill event.code from keyCode using a lookup table, or use a library that abstracts the difference.

Modifier Key Normalization: Mac vs Windows/Linux

The largest cross-platform keyboard headache is the Command key on macOS. Mac users expect Cmd+S to save; Windows and Linux users expect Ctrl+S. The browser exposes four boolean modifier properties on every keyboard event:

  • event.metaKeytrue when Command (Mac) or Windows key is held. On Mac this is the shortcut modifier.
  • event.ctrlKeytrue when Control is held. On Windows/Linux this is the shortcut modifier; on Mac it is a secondary modifier rarely used for application shortcuts.
  • event.altKey — Option on Mac, Alt on Windows/Linux.
  • event.shiftKey — Shift on all platforms.

The correct cross-platform pattern is to check metaKey on Mac and ctrlKey everywhere else. The platform detection approach:

const isMac = navigator.platform.startsWith('Mac')
  || navigator.userAgentData?.platform === 'macOS';

function isPrimaryModifier(e) {
  return isMac ? e.metaKey : e.ctrlKey;
}

Keyboard Shortcut Libraries

Rolling your own shortcut system is error-prone. Three popular libraries handle the normalization for you:

LibrarySizeBest forNotes
hotkeys-js~2 kBVanilla JS, any frameworkSimple API, scope support, no dependencies
react-hotkeys-hook~3 kBReact hooksRespects React lifecycle; disables in inputs by default
Tinykeys~0.6 kBMinimal footprintKey sequence support (g i style), uses event.key
// hotkeys-js
import hotkeys from 'hotkeys-js';
hotkeys('ctrl+s, command+s', (e) => {
  e.preventDefault();
  save();
});

// react-hotkeys-hook
import { useHotkeys } from 'react-hotkeys-hook';
useHotkeys('mod+s', () => save(), { preventDefault: true });

// Tinykeys
import { tinykeys } from 'tinykeys';
tinykeys(window, {
  '$mod+s': (e) => { e.preventDefault(); save(); },
});

All three treat mod or command as the platform-appropriate primary modifier, handling the Mac/Windows split automatically.

Common Pitfalls

  • Forgetting preventDefault for browser shortcuts: Binding Ctrl+S without calling e.preventDefault() will both trigger your handler and open the browser's Save dialog.
  • International keyboards and special characters: On a German keyboard, / requires Shift+7. If you match on event.key === "/" you are fine; if you match on event.code === "Slash" the German user needs to use Shift+7 consciously. Decide which behavior you want.
  • Dead keys: Keys like ^ or ` on European layouts are dead keys — they do not produce a character on their own but combine with the next keystroke. event.key returns "Dead" for these. Handle or ignore them explicitly.
  • keydown vs input event ordering: The keydown event fires before the input value changes. If you read input.value inside a keydown handler, you see the value before the keystroke. Use the input event when you need the updated value.
  • Shortcuts firing inside inputs: Global shortcut listeners catch keystrokes even when focus is inside a text input or contenteditable. Guard with e.target.tagName or use a library that handles this.

Full Cross-Platform Shortcut Matcher

The function below combines platform detection, modifier normalization, and IME-awareness into a reusable shortcut handler:

const isMac =
  typeof navigator !== 'undefined' &&
  (navigator.platform.startsWith('Mac') ||
   navigator.userAgentData?.platform === 'macOS');

/**
 * Returns true when the keyboard event matches the shortcut descriptor.
 *
 * descriptor examples:
 *   'mod+s'         → Cmd+S on Mac, Ctrl+S elsewhere
 *   'mod+shift+k'   → Cmd+Shift+K on Mac, Ctrl+Shift+K elsewhere
 *   'escape'        → Escape key, no modifiers
 */
function matchesShortcut(e, descriptor) {
  // Skip during IME composition
  if (e.isComposing || e.key === 'Process') return false;

  const parts = descriptor.toLowerCase().split('+');
  const key = parts[parts.length - 1];
  const wantsMod   = parts.includes('mod');
  const wantsShift = parts.includes('shift');
  const wantsAlt   = parts.includes('alt');

  const primaryModifier = isMac ? e.metaKey : e.ctrlKey;
  const conflictingMod  = isMac ? e.ctrlKey  : e.metaKey;

  if (wantsMod   !== primaryModifier) return false;
  if (conflictingMod)                 return false;
  if (wantsShift !== e.shiftKey)      return false;
  if (wantsAlt   !== e.altKey)        return false;

  return e.key.toLowerCase() === key;
}

// Usage
document.addEventListener('keydown', (e) => {
  if (matchesShortcut(e, 'mod+s')) {
    e.preventDefault();
    save();
  }
  if (matchesShortcut(e, 'mod+shift+k')) {
    e.preventDefault();
    deleteLine();
  }
});

Use the Keyboard Event Tester to inspect the exact key, code, keyCode, and modifier flags your browser fires for any keystroke — useful when debugging layout-specific behavior or verifying shortcut logic before shipping.