A keyboard shortcut that works on your laptop can fail for a user in Paris, on a phone, or on a Mac. Every keydown event carries three ways to name the key, and each one answers a different question. Picking the right one is most of the work.
Keep the JavaScript Keycode Finder open as you read: press a key and it shows every property below.
What each property tells you
event.key is the value the key produces with the current layout and modifiers. For a character key it is the character itself: "a", "A", "é" or " " for Space. For other keys it is a name such as "Enter", "ArrowLeft", "Shift" or "F5". The W3C Recommendation "UI Events KeyboardEvent key Values" lists the names.
event.code names the physical key, after its position on a US keyboard. The key right of Tab is "KeyQ" on every layout, and the top-row 1 is "Digit1" whether it types 1, ! or &. These names come from "UI Events KeyboardEvent code Values", also a W3C Recommendation.
event.keyCode and event.which are older numbers. On keydown and keyup they hold the same value, such as 13 for Enter or 65 for A. charCode is the character's code on the keypress event only. The UI Events specification keeps all three in its legacy section so old pages keep working.
| Property | What it holds | Q-position key on AZERTY | Shift+1 on a US layout | Status |
|---|---|---|---|---|
key | The character typed, or a key name | "a" | "!" | Standard |
code | The physical key, named for its US position | "KeyQ" | "Digit1" | Standard |
keyCode, which | A legacy number that varies by browser | 65 | 49 | Deprecated |
charCode | The character's code, on keypress only | 97 | 33 | Deprecated |
Two more properties help. event.location is 1 or 2 for the left or right Shift, Control, Alt and Meta, 3 for the numeric keypad and 0 for everything else, and event.repeat is true while a held key auto-repeats.
Layouts: why games use code and text uses key
French AZERTY swaps A with Q and Z with W, and types & on the 1 key unless Shift is held. German QWERTZ swaps Y and Z. On Dvorak, the key in the S position types o. In each case key follows the layout and code doesn't.
That makes code right for anything tied to position. A game that moves with WASD should check "KeyW", "KeyA", "KeyS" and "KeyD", which are the same four keys in the same shape on AZERTY, where they are labelled Z, Q, S and D. To tell players which keys those are, Chromium browsers offer navigator.keyboard.getLayoutMap(), which maps a code to the label on their keyboard.
const moves = { KeyW: [0, -1], KeyA: [-1, 0], KeyS: [0, 1], KeyD: [1, 0] };
document.addEventListener("keydown", (event) => {
const move = moves[event.code];
if (!move) return;
event.preventDefault();
player.step(...move);
});
Text is the opposite: the user means the character, so key is right. Shortcuts named after a letter usually follow key too, because Ctrl+Z should be the key with Z printed on it. On non-Latin layouts such as Russian, key can be a Cyrillic letter, so many apps also accept the matching code as a fallback.
Shift, AltGr and Option change key, not code
Modifiers change the character, so they change key. Shift+A gives "A" and Shift+1 gives "!", while code stays "KeyA" or "Digit1" and keyCode stays 65 or 49. To match a letter shortcut whatever the Shift or Caps Lock state, compare event.key.toLowerCase().
Many European layouts type extra characters with AltGr, the right Alt key. On a German layout AltGr+Q types @. When a layout uses AltGr, the right Alt key reports key as "AltGraph", and on Windows the browser also sends a Control keydown, so ctrlKey and altKey are both true while @ is typed. A Ctrl+Alt+Q shortcut matched by code would fire whenever a German user types an email address.
On a Mac, Option is the Alt key and types characters too: Option+S gives "ß", and Option+E is a dead key, reported as "Dead", that adds an accent to the next letter. Match Option shortcuts with event.code and event.altKey.
Why keyCode, which and keypress are deprecated
keyCode was never standardised across browsers. Letters, digits, Enter, Tab, Escape and the arrows agree everywhere, but punctuation doesn't: Chrome, Edge and Safari send 186, 187 and 189 for ;, = and -, while Firefox sends 59, 61 and 173. Some keys also get different numbers on a Mac. MDN marks keyCode, which and charCode deprecated, and the key code table in the tool notes where browsers disagree.
keypress is deprecated as well. It fires only for keys that produce a character, plus Enter, so Escape and the arrows never reach it, and it misses text from paste, autocorrect and input methods. Use keydown for keys and beforeinput or input for text.
Browsers still send all of it: every keydown has a keyCode, keypress still fires, and jQuery-era code still reads event.which. Replacing them is mechanical:
// Before
if (event.keyCode === 13) submit();
if (event.which === 27) close();
// After
if (event.key === "Enter") submit();
if (event.key === "Escape") close();
For letters, the old number is the ASCII code of the uppercase letter, which you can check in the ASCII table.
IME composition and keyCode 229
Chinese, Japanese and Korean users type through an input method editor (IME), which composes text before committing it. While an IME is processing a key, the UI Events specification tells browsers to report keyCode 229, and key is often "Process". Events between compositionstart and compositionend have isComposing set to true.
Two details catch people. The keydown that starts a composition fires before compositionstart, so its isComposing is false even though its keyCode is 229. And the Enter that confirms the text sends a keydown too, which submits the form if you only check event.key === "Enter". Safari has long reported that confirming keydown with isComposing false but keyCode 229. Test both:
input.addEventListener("keydown", (event) => {
if (event.isComposing || event.keyCode === 229) return;
if (event.key === "Enter") sendMessage();
});
Many phone keyboards, especially on Android, compose every word, so keydown arrives with key "Unidentified" and keyCode 229 even for plain letters. Read the text from the input event: its inputType says what happened, such as "insertText" or "deleteContentBackward", and its data holds the inserted text.
Mac and Windows modifiers
metaKey is ⌘ Command on a Mac and the Windows key on Windows; Firefox reports the Windows key as Meta from version 118. Mac users expect ⌘ where Windows and Linux users expect Ctrl, so the common pattern accepts either:
document.addEventListener("keydown", (event) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
event.preventDefault(); // stop the browser's Save dialog
save();
}
});
Two limits apply. Some shortcuts never reach the page, such as closing or switching tabs, or the operating system's own Windows-key and ⌘ shortcuts. And on a Mac, a key released while ⌘ is held often sends no keyup, so don't make a ⌘ shortcut wait for one.
Keep the keyboard accessible
- Don't call
preventDefault()on Tab or Shift+Tab unless your widget moves focus itself and offers a way out. WCAG 2.1.2, No Keyboard Trap, requires that keyboard users can always leave a component. - Use a
<button>for anything clickable. It already activates with Enter and Space, so you need no key handler at all. - Ignore shortcuts while the user is typing: return early when
event.targetis an input, a textarea, a select or an element withisContentEditable. - Single-key shortcuts, such as / to search, need a way to turn them off or remap them, or must only work while their component has focus, under WCAG 2.1.4, Character Key Shortcuts.
- Document shortcuts with the
aria-keyshortcutsattribute and in visible text.
Which one to use
Use key for text and for shortcuts named after a character, code for games and position-based controls, and location when left and right or the numpad matter. Keep keyCode only for the 229 check. When you're unsure what a key sends on a given layout, press it in the Keycode Finder and copy the check it writes.