key vs code vs keyCode — the three identities of a keypress
Press one key and the event carries several different descriptions of it. Knowing which to read is most of the battle:
| Property | Tells you | Layout-dependent? | Status |
|---|---|---|---|
event.key | The character/value produced ("a", "A", "Enter") | Yes | Use this |
event.code | The physical key position ("KeyA", "Space") | No | Use this |
event.keyCode | Legacy numeric code | Yes | Deprecated |
keyCode is deprecated and shouldn’t be used in new code, but it’s still shown here for maintaining legacy systems. For everything new, it’s key (what was typed) and code (which physical key) — and the FAQ above explains which to pick.
Detecting combinations
A shortcut like Ctrl+Shift+S is just a key/code check combined with the modifier booleans the event always carries: event.ctrlKey, event.shiftKey, event.altKey, and event.metaKey (Command on macOS, Windows key on Windows). This viewer generates the exact condition for whatever combination you press, including the cross-platform nuance that “Ctrl” on Windows is often “Cmd” (metaKey) on macOS — a detail that trips up a lot of shortcut code.
Why some keys never reach your handler
If a key seems “dead,” it may be intercepted before JavaScript sees it. PrintScreen is grabbed by the OS, F11 toggles browser fullscreen, and Ctrl+W closes the tab — and preventDefault() can’t always override these reserved combinations. When designing shortcuts, prefer combinations (Ctrl+Shift+letter) that browsers don’t already claim, and test on the platforms you support, since the reserved set differs between OSes and browsers.
The IME bug that breaks shortcut handlers for a billion users
This is the single most valuable thing an event viewer can show you, and you will only see it if you test with an input method editor active.
When a user is composing text with an IME — Japanese, Chinese, Korean, and predictive input on many Android keyboards — the browser fires keydown for each physical keypress, but it does not report the key. Chrome and Edge report keyCode 229 and key as "Process"; the composition is still in progress and the actual character does not exist yet. If your shortcut handler branches on event.key, it sees a value it has no case for. If it branches on keyCode, it sees 229 for every single keystroke.
The consequence is a class of bug that is invisible to English-only testing: a Cmd+K palette that opens while a Japanese user is mid-word, or a shortcut that silently never fires for them. The fix is one line, and it is the reason isComposing exists:
input.addEventListener('keydown', (e) => {
if (e.isComposing || e.keyCode === 229) return; // composition in progress
if (e.key === 'Enter') submit();
});
Check both: isComposing is the standard and correct test, and the keyCode === 229 fallback covers older engines that fire keydown before compositionstart. Press keys here with an IME enabled and watch the isComposing flag flip — that is the behaviour your handler has to survive.
On macOS, Cmd swallows your keyup
A related trap with no spec-level fix. While the Command key is held down on macOS, Safari and Chrome do not deliver keyup for other keys. Press Cmd+S and you will see keydown for s in the log above, then keyup for Meta — and no keyup for s at all.
Any handler that tracks “which keys are currently down” in a Set, adding on keydown and removing on keyup, will therefore leak: s stays in the set forever after the first Cmd+S. Games, drawing tools and chord-based shortcut systems all hit this. The workaround is to clear the tracked set whenever Meta is released, and on window.blur — because the same leak happens when a key is held while the tab loses focus.
The focus gotcha
Key events fire on the focused element first. A document-level listener may not behave as expected when an <input>, <textarea>, or contenteditable has focus and consumes the event. For global shortcuts, check event.target.tagName to decide whether to act or defer, or attach the listener with { capture: true } to see the event on the way down. Combined with the isComposing guard above, that’s the recipe for shortcuts that don’t fight with text entry.