In a KeyboardEvent, what is the difference between event.key and event.code, and which one should a keyboard shortcut match against?
answer
- meaning versus position
- layout-aware or layout-independent
- Shift changes one, not the other
- AZERTY prints W where QWERTY has Z
- key for mnemonics, code for WASD
basics
~20 sKeyboardEvent.key is the layout-aware value produced by the keystroke, such as "a", "A" or "Enter"; KeyboardEvent.code is the physical key position, such as "KeyA" or "Space", and never changes with layout. Match text shortcuts on key and positional controls on code.
solid answer
~50 s`event.key` reports what the keystroke *means* under the user's current layout and modifiers: the character `"a"`, or `"A"` with Shift, or a named value like `"Enter"`, `"Escape"`, `"ArrowLeft"`, `"Tab"`. `event.code` reports *which physical key* was pressed, using a fixed US-QWERTY-based naming scheme — `"KeyA"`, `"Digit1"`, `"Space"`, `"ShiftLeft"` — and it is identical no matter what layout the user has installed. So on a French AZERTY keyboard, the key printed W sits where QWERTY has Z: it reports `code: "KeyZ"` and `key: "w"`. That is the whole decision rule. A Ctrl+Z undo shortcut should match `event.key === 'z'`, so it lands on the key the user thinks of as Z. A WASD movement scheme should match `code`, so the four keys stay in the same physical diamond on every layout. `keyCode` and `which` are deprecated and should not be used in new code.
code
javascript · 16 linesdocument.addEventListener('keydown', (event) => {
if (event.isComposing || event.repeat) return;
const mod = event.metaKey || event.ctrlKey; // Cmd on macOS, Ctrl elsewhere
// Mnemonic shortcut: follow the letter the user sees.
if (mod && event.key.toLowerCase() === 'k') {
event.preventDefault();
console.log('open command palette');
}
// Positional control: follow the physical key.
if (event.code === 'KeyW') {
console.log('move forward');
}
});go deeper
Recall that key gives the character or a name like Enter, while code gives the physical key such as KeyA, and that keyCode is deprecated. Reading both off a logged event is enough at this level.
Explain why code is named after US-QWERTY positions and stays fixed under Shift or a different layout, and justify out loud which one a given shortcut should match.
Demonstrate production judgment: normalise case, treat metaKey and ctrlKey together for cross-platform shortcuts, skip auto-repeat with event.repeat, and guard IME composition before acting on Enter.
Own the strategy for an app-wide shortcut layer — a single registry with a declarative binding format, a policy for which bindings are mnemonic versus positional, and a plan for user remapping and conflicts with browser and assistive-technology shortcuts.
## Two questions about one keystroke When a key goes down, the browser can tell you two independent things: *what character or command this produced*, and *which button on the slab of plastic was pressed*. `KeyboardEvent.key` answers the first; `KeyboardEvent.code` answers the second. Nearly every keyboard bug comes from asking one and needing the other. ## event.key — the meaning `key` is a string that already accounts for the active layout and the modifier state: - Printable keys yield the character produced: `"a"` alone, `"A"` with Shift, `"@"` on Shift+2 for a US layout. - Non-printable keys yield a named value from a fixed vocabulary: `"Enter"`, `"Escape"`, `"Tab"`, `"Backspace"`, `"ArrowUp"`, `"Home"`, `"F5"`, `"Shift"`, `"Control"`. - A dead key (the accent-composing keys on many European layouts) yields `"Dead"`. - If the browser cannot determine a value it yields `"Unidentified"`. Because `key` is case-sensitive, a naive `event.key === 'k'` check silently fails when Caps Lock is on or Shift is held. Normalise with `event.key.toLowerCase()` when the shortcut is meant to be case-insensitive. ## event.code — the position `code` names the physical key using a layout-independent scheme borrowed from US QWERTY positions: `"KeyA"` … `"KeyZ"`, `"Digit0"` … `"Digit9"`, `"Space"`, `"Enter"`, `"ArrowLeft"`, `"Semicolon"`, `"Backquote"`, and left/right-distinguished modifiers `"ShiftLeft"` / `"ShiftRight"`, `"ControlLeft"` / `"ControlRight"`. Two consequences follow. First, `code` is unaffected by Shift: the same physical key is `"Digit2"` whether it produced `2` or `@`. Second, `code` is *labelled by QWERTY geography*, not by what is printed on the user's keycap. On AZERTY the top-left letter key is printed A but reports `"KeyQ"`; the key printed W reports `"KeyZ"`. A Dvorak user sees an even bigger scramble. ```js document.addEventListener('keydown', (e) => { console.log(e.key, e.code); // AZERTY, key printed "W": "w" "KeyZ" }); ``` ## Choosing between them The rule is about what the shortcut *is*. - **Mnemonic / text shortcuts** — Ctrl+Z for undo, Ctrl+B for bold, `/` to focus search — are about the letter the user has in mind. Match `event.key`. Matching `code` here means a French user must press the key printed W to undo, which is exactly the bug. - **Positional controls** — WASD movement, or a spatial key cluster in a game or editor — are about where the fingers sit. Match `event.code`, so the shape survives layout changes. - **Named keys** — Escape, Enter, arrows, Tab — are the same in both, and `key` reads better: `if (e.key === 'Escape') close();`. Always gate on modifiers explicitly: `e.ctrlKey`, `e.metaKey`, `e.shiftKey`, `e.altKey`. Because macOS uses Command where Windows and Linux use Control, the portable form is `const mod = e.metaKey || e.ctrlKey;`. `e.getModifierState('CapsLock')` covers lock keys that are not exposed as booleans. ## The deprecated properties `keyCode`, `charCode` and `which` are legacy numeric properties whose values were never consistently specified across browsers and layouts; the UI Events specification marks them deprecated. Likewise the `keypress` event is deprecated. For "a key went down" use `keydown`; for "text is about to change" use `beforeinput`, and for "text changed" use `input` on the field. Those input events are the correct level for anything that cares about the resulting *text*, since they also fire for paste, autofill, dictation and on-screen keyboards where no physical key exists. ## Traps worth naming - **IME composition.** While an input method editor is composing (Japanese, Chinese, Korean), `keydown` fires with `key` set to `"Process"` or `"Unidentified"` and the legacy `keyCode` of 229. Guard with `if (e.isComposing) return;` before acting on Enter, or you will submit a form while the user is still choosing a candidate. - **Auto-repeat.** Holding a key fires repeated `keydown` events with `event.repeat === true`; skip them for actions that should fire once. - **Virtual keyboards.** Soft keyboards and assistive tooling may not produce a meaningful `code` at all — it can be the empty string — which is another argument for `key` in general-purpose shortcuts. - **Order.** `keydown` fires before the value changes; `keyup` fires after. Reading `input.value` in `keydown` gives you the value *before* this keystroke.
- Why is checking event.keyCode === 13 for Enter considered wrong in new code?`keyCode` is deprecated in the UI Events specification: its values were never consistently defined across browsers and layouts, and it conflates character and key information. `event.key === 'Enter'` is specified, self-documenting, and works identically everywhere. The only reason to still see `keyCode` is legacy code or an IME check for the value 229.
- A form submits on Enter, but Japanese users report it firing while they are still picking a candidate. What is the fix?That Enter is confirming an IME composition, not submitting. The keydown arrives with `isComposing` true (legacy `keyCode` 229), so guard the handler with `if (event.isComposing) return;`. The `compositionstart` and `compositionend` events let you track the same state explicitly if you need it beyond one handler.
- Your shortcut is Ctrl+K but nothing happens when Caps Lock is on. Why?`event.key` is case-sensitive, so with Caps Lock the value is `"K"` and a strict `=== 'k'` comparison fails. Compare `event.key.toLowerCase() === 'k'`, and keep the modifier check separate. Matching on `event.code === 'KeyK'` also sidesteps case, at the cost of following physical position rather than the printed letter.
- Should a shortcut listener use keydown or keyup?`keydown` for essentially all shortcuts: it fires before the default action, so `preventDefault()` can still suppress the browser's own behaviour, and it matches the user's sense of when the command fires. `keyup` cannot cancel a default and misses auto-repeat entirely; reserve it for things like "released the modifier".
saying these in an interview costs you the question
- Says code is just key with the layout applied
- Believes event.code changes when Shift is held
- Uses keyCode numbers for new shortcut code
- Matches a Ctrl+Z shortcut on code and breaks non-QWERTY layouts
- Ignores isComposing and submits during IME composition