key is what you typed, code is where you pressed
event.key is the character or named action the keystroke produced after the operating system applied the keyboard layout and the modifiers. Press the key marked A and you get "a"; hold Shift and the same key gives "A". Non-character keys report names instead: "Enter", "ArrowLeft", "Escape", "F5", and a single space for the space bar.
event.code is the physical key, named after its position on a notional US QWERTY board and independent of layout, modifiers and language. The key in the position where US QWERTY has A always reports "KeyA" — on a French AZERTY board that is the key labelled Q, and pressing it gives key: "q" with code: "KeyA". On Dvorak the same physical key produces "a" again but sits somewhere else entirely under the fingers. Digits report "Digit1" along the top row and "Numpad1" on the keypad, which is how you tell those two apart at all.
The choice between them is a choice about what you actually mean. A text editor shortcut on Ctrl+S means the letter S, so match on key and it keeps working when a user switches layout. A game binding WASD means those four positions under the left hand, so match on code and a French player gets ZQSD in the same physical square rather than a scattered mess. Matching WASD on key is the single most common cause of "the controls do not work on my keyboard" bug reports.
keyCode is deprecated, and was never reliable
keyCode and its twin which are numeric identifiers from the pre-standard era. The specification lists them as deprecated and defines them only for legacy compatibility. They are still populated in every browser, which is the reason the habit persists, but they conflate the two ideas above without committing to either: the number reflects a physical key on some layouts and a produced character on others, values disagree across browsers for punctuation, and Firefox historically reported 0 for character keys in keypress while other engines reported the character code.
There is no reason to write new code against them. The mapping is key for characters, code for positions, and that covers everything keyCode was ever used for. The panel shows the numbers because you will meet them while reading old code and need to know what 13, 27 and 32 correspond to.
Modifiers, location and repeat
Shift, Ctrl, Alt and Meta arrive as four booleans on every event, not as part of key. Meta is the Command key on macOS and the Windows key elsewhere, and cross-platform shortcut code usually tests e.metaKey || e.ctrlKey rather than picking one. Pressing a modifier on its own also fires keydown, with key set to the modifier's name — worth knowing if you are building a shortcut recorder, since you have to ignore those events until a non-modifier arrives.
location disambiguates keys that exist twice: 1 for left, 2 for right, 3 for the numpad, 0 for everything else. Left and right Shift share key: "Shift" and differ in code as well, so location is mostly redundant now, but numpad Enter is a case where it earns its keep — it reports key: "Enter" exactly like the main Enter and only code: "NumpadEnter" tells them apart.
repeat is true for the stream of events the operating system generates while a key is held. Anything that should fire once per press — toggling a panel, firing a weapon, submitting a form — needs to check it, or holding the key will fire it thirty times a second.
IME composition, and the double-submit bug
While an input method editor is composing — Korean, Japanese, Chinese, and anything else that assembles characters from a sequence of keystrokes — keydown fires with key set to "Process" and isComposing set to true. The keystroke belongs to the composition, not to your application.
The classic bug is a chat box that sends on Enter. A user presses Enter to confirm the composed characters, the handler sees Enter and sends a half-finished message, and then the real Enter sends it again. The guard is one line at the top of the handler — if (e.isComposing) return; — and it is missing from a great deal of shipped code.
What you cannot capture
Some combinations never reach the page. The operating system takes them first: Win+D, Cmd+Tab, Cmd+Space, Ctrl+Alt+Delete. The browser takes others for itself, and although preventDefault() can suppress a surprising amount, it cannot suppress the ones that close the tab or open a native menu, and it should not try. This panel deliberately leaves Tab, the function keys used for reload and devtools, and every Ctrl or Meta combination unblocked, so that you can still leave the page you are testing on.
Questions people ask
Nothing happens when I press a key.
The panel has to be open — press the button so the result area renders, since the handler checks for its presence before doing anything. Focus also matters in one direction only: keys are captured from anywhere on the page except a textarea or a non-text input, where they are left alone so those controls still work normally.
Should I ever use keyCode in new code?
No. Use key when you care about the character and code when you care about the physical position. keyCode is deprecated in the specification, varies between browsers for punctuation and symbols, and does not cleanly represent either of the two things you might mean. It is populated only so that decade-old code keeps running.
How do Windows and Command keys report?
key is "Meta" on both, code is "MetaLeft" or "MetaRight", and metaKey is true while it is held. Combinations the operating system claims for itself never arrive at all, so you cannot test Cmd+Tab or Win+D here — that is the OS intercepting, not the browser.
Why does the same key give different values on my colleague's machine?
Layout. code is defined against a US QWERTY reference, so it stays constant across layouts while key follows whatever the user has configured. On AZERTY the physical KeyA position produces "q"; on QWERTZ the Y and Z positions are swapped relative to their labels. If your shortcut documentation says "press Z" and your code tests code === "KeyZ", those two statements are only the same sentence on some keyboards.