Keyboard: keydown and keyup
Before we touch a single key, a warning about scope. On a modern device, typing is only one of many ways text lands in a field. People dictate with speech recognition, paste with the mouse, or drop text in from another app. None of those go through the keyboard.
So if your goal is to know what a user typed into an <input>, keyboard events will miss most of the ways text actually arrives. There’s a dedicated event for that job, input, which fires on any change to the field’s value regardless of how the change happened. We cover it in Events: change, input, cut, copy, paste, and for tracking field content it’s the right tool.
Keyboard events are for handling the keyboard itself (a virtual on-screen keyboard counts too). Reacting to the arrow keys Up and Down, wiring up a hotkey like Ctrl+S, catching Escape to close a dialog: that’s their territory.
Teststand
The fastest way to build intuition here is to watch the events fire. The teststand below dumps every keyboard event with its properties as you type. Try lowercase and uppercase, hold a key down, press Shift alone, mash a hotkey combination, and see what each event reports.
keydown and keyup
Two events bracket every key press. keydown fires the moment a key goes down; keyup fires when it comes back up. Whatever you do in between (moving the mouse, pressing other keys) happens while that key is still considered “down”.
event.code and event.key
Every keyboard event carries two properties that answer two different questions.
event.keyis the character the key produces right now.event.codeis the physical key on the board, independent of what character it types.
Why two? Because the same physical key produces different characters depending on modifiers and language. Take the Z key. Press it plain and you get z. Hold Shift and you get Z. The character changed, but you pressed the same physical button in the same spot.
| Key | event.key |
event.code |
|---|---|---|
| Z | z (lowercase) |
KeyZ |
| Shift+Z | Z (uppercase) |
KeyZ |
Switch your OS to a different language and that key might type something else entirely — a Cyrillic or Greek letter, say. event.key follows the character, so it changes. event.code stays "KeyZ" because the physical key never moved.
Now, what about keys that don’t produce any character at all — Shift, F1, Backspace? There’s no letter to report, so event.key falls back to a name that’s roughly the same as event.code:
| Key | event.key |
event.code |
|---|---|---|
| F1 | F1 |
F1 |
| Backspace | Backspace |
Backspace |
| Shift | Shift |
ShiftRight or ShiftLeft |
Look closely at that last row. event.code pins down exactly which key you pressed. Most keyboards have two Shift keys, one on each side, and the code tells them apart: ShiftLeft versus ShiftRight. event.key reports only the meaning — both are just "Shift", because as far as the character goes, they do the same thing.
key or code: which do you check?
Suppose you want the classic undo hotkey: Ctrl+Z on Windows and Linux, or Cmd+Z on Mac. You listen on keydown and inspect the event. But do you test event.key or event.code?
The argument for event.code: it’s stable. Because it’s tied to a physical location rather than a character, it survives the user switching keyboard languages mid-session. Someone flips their OS from English to Russian, and your KeyZ check keeps working.
document.addEventListener('keydown', function(event) {
if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) {
alert('Undo!');
}
});
Try the same idea live. Click into the box below to focus it, then press the undo hotkey. Because the check is on event.code == 'KeyZ', it fires on the same physical key regardless of your keyboard language:
But event.code has its own trap. Different keyboard layouts put different characters on the same physical position. Compare the US layout (“QWERTY”) with the German layout (“QWERTZ”):
On the US board the key next to the left Shift types Z; on the German board that very same physical key types Y. The Y and Z positions are swapped.
The consequence is odd but real: for a German-layout user, pressing Y fires an event with event.code == 'KeyZ'. So a check for event.code == 'KeyZ' triggers on the key that visibly says Y. The specification documents this behavior openly.
The good news: only a handful of codes swap around like this — KeyA, KeyQ, KeyZ and a few neighbors. Special keys such as Shift, Enter, and the arrows are safe; their codes mean the same thing everywhere. The full list is in the spec.
So there’s no universal winner. Pick by intent:
Auto-repeat
Hold a key down long enough and the OS starts repeating it: the keydown event fires over and over, once per repeat. Only when you finally release the key do you get a single keyup. So one physical press-and-hold produces many keydown events and one keyup.
Every event fired by auto-repeat carries event.repeat == true. The very first keydown (the genuine press) has event.repeat == false. If you want an action to happen exactly once per press and not machine-gun while the key is held, guard on it:
document.addEventListener('keydown', function(event) {
if (event.repeat) return; // ignore auto-repeats
// ...runs once per real press
});
Default actions
A keypress can kick off all sorts of built-in behavior, and there are many possibilities:
- A character shows up on screen (the obvious one).
- A character gets deleted (Delete).
- The page scrolls (PageDown).
- The browser opens its “Save Page” dialog (Ctrl+S).
- …and plenty more.
Calling preventDefault() on keydown cancels most of these. The exception is keys the operating system claims for itself. On Windows, Alt+F4 closes the browser window, and JavaScript can’t stop it — the event never reaches your page as something you can veto.
Here’s a practical use: an <input> that only accepts characters valid in a phone number — digits and +, (, ), -.
<script>
function checkPhoneKey(key) {
return (key >= '0' && key <= '9') || ['+','(',')','-'].includes(key);
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" type="tel">
The onkeydown handler runs checkPhoneKey on the pressed key. Valid keys (a digit 0–9 or one of +-()) return true; anything else returns false.
Returning false from a handler assigned via a DOM property or an inline attribute cancels the default action. So for any key that fails the test, nothing appears in the field. (Returning true does nothing special; only false has an effect here.)
There’s a catch you can feel immediately: Backspace, Left, and Right stop working inside that input. Those keys aren’t digits or symbols, so checkPhoneKey returns false and their default behavior gets cancelled too. You’ve filtered out editing along with the bad input.
Loosen the filter to let editing keys through:
<script>
function checkPhoneKey(key) {
return (key >= '0' && key <= '9') ||
['+','(',')','-','ArrowLeft','ArrowRight','Delete','Backspace'].includes(key);
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" type="tel">
Now arrows and deletion behave, and the digit filter still holds. Type in the field below: letters and other characters are silently rejected, while digits, + ( ) -, and the editing keys go through. Edit the allow-list in the code to feel how the filter shapes what lands in the field:
The robust approach is to also watch the oninput event, which fires after any change, however it happened. There you read the current input.value, and reject or highlight it if it’s invalid. Using both together gives you a smooth typing experience plus a reliable final check.
Legacy
There used to be a keypress event, along with the keyCode, charCode, and which properties on the event object. Browsers implemented them so inconsistently that the spec authors gave up and deprecated the whole lot, replacing them with the modern keydown/keyup model and key/code described above. Old code still runs — browsers keep the deprecated features alive for compatibility — but there’s no reason to reach for them in new code.
Mobile keyboards
Virtual and mobile keyboards, formally called IMEs (Input-Method Editors), play by looser rules. The W3C standard says that for input coming through an IME, a KeyboardEvent’s keyCode should be 229 and its key should be "Unidentified" — placeholder values that carry no useful detail. See the UI Events spec for the specifics.
In practice, some mobile keyboards do report sensible values for e.key and e.code on certain keys, like arrows or backspace, but there’s no guarantee. So keyboard logic that works flawlessly on desktop may behave unpredictably on a phone. If you need to know what changed in a field on mobile, the input event is the dependable choice.
Summary
Pressing any key generates a keyboard event, whether it’s a symbol key or a modifier like Shift or Ctrl. The one common exception is the Fn key found on some laptops: it produces no keyboard event, because it’s usually handled below the OS level, in hardware.
The two events:
keydown— the key goes down (auto-repeats while held).keyup— the key is released.
The two main properties:
code— the physical key’s identity ("KeyA","ArrowLeft", …), tied to its location on the board.key— the character produced ("A","a", …); for non-character keys such as Esc it usually mirrorscode.
Keyboard events were once pressed into service to track what users typed into form fields. That’s unreliable, because input arrives through many channels — paste, dictation, autofill. For field content, use the input and change events (see Events: change, input, cut, copy, paste); they fire after any change, whatever its source.
Reach for keyboard events when you genuinely mean the keyboard: hotkeys, arrow-key navigation, and reactions to specific physical keys.