skip to content

Pointer, Mouse, Keyboard, and Touch Events

You will learn the input event families the platform exposes and the coordinate and key properties that trip people up. Interviewers ask because drag, scroll, and keyboard-shortcut features are where event knowledge stops being theoretical.

on this pageshow

questions

5

In a KeyboardEvent, what is the difference between event.key and event.code, and which one should a keyboard shortcut match against?

level: middleimportance: must knowfreq 62%

answer

  1. meaning versus position
  2. layout-aware or layout-independent
  3. Shift changes one, not the other
  4. AZERTY prints W where QWERTY has Z
  5. key for mnemonics, code for WASD

basics

~20 s

KeyboardEvent.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 lines
javascript
document.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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

In a browser MouseEvent, what do screenX, clientX, pageX and offsetX each measure, and how do you reliably get the pointer position relative to a specific element?

level: juniorimportance: should knowfreq 55%

basics

~20 s

They differ only in origin: screenX is measured from the screen, clientX from the viewport, pageX from the document (clientX plus scroll offset), and offsetX from the padding edge of the event target. For element-local coordinates, subtract getBoundingClientRect().left from clientX.

open as a page

A touchmove listener added on document calls event.preventDefault() to stop the page scrolling, but the page scrolls anyway and the console warns that preventDefault cannot be used inside a passive listener. Why, and what are the options?

level: middleimportance: should knowfreq 42%

basics

~20 s

Browsers make touchstart, touchmove and wheel listeners passive by default when they are registered on window, document, document.documentElement or document.body, so preventDefault() is ignored. Opt back in with { passive: false }, or block the gesture declaratively with CSS touch-action.

open as a page

A card in a web page registers handlers for both touchstart and mousedown; on a phone the action runs twice. Why does that happen, and how do Pointer Events avoid the problem?

level: middleimportance: should knowfreq 48%

basics

~20 s

Touchscreen browsers fire the touch sequence and then replay it as compatibility mouse events so older mouse-only pages keep working, so both handlers run. Pointer Events replace both families with one stream — pointerdown, pointermove, pointerup — tagged by pointerType.

open as a page

A custom slider tracks a drag with pointerdown on the thumb plus pointermove and pointerup on the same element. The drag stops updating as soon as the pointer moves off the thumb. What does Element.setPointerCapture() change, and what else must the handler deal with?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Without capture, pointermove is dispatched by hit-testing, so it goes to whatever element is under the pointer once it leaves the thumb. setPointerCapture(pointerId) retargets every later event for that pointer to the capturing element until pointerup or pointercancel, which must also reset the drag state.

open as a page