ESC

Type to search the knowledge base.

preventDefault vs stopPropagation

Cancel browser defaults vs stop event bubbling — capture, stopImmediatePropagation, and when passive listeners ignore preventDefault.

beginner3 min read
  • javascript
  • preventdefault-vs

Click a link — browser navigates. Submit a form — browser posts. Those are default actions. Events also propagate through the DOM (capture → target → bubble). Two different APIs control those two behaviors, and interviews mix them up constantly.

preventDefault — cancel the default action

link.addEventListener('click', (e) => {
  e.preventDefault();
  // SPA route change instead of full navigation
  router.push(link.getAttribute('href'));
});

form.addEventListener('submit', (e) => {
  e.preventDefault();
  submitWithFetch(new FormData(form));
});

Does not stop the event from bubbling. Parent listeners still run unless you also stop propagation.

e.defaultPrevented; // true after preventDefault in the same dispatch

stopPropagation — stop traveling the tree

button.addEventListener('click', (e) => {
  e.stopPropagation();
  // document-level click-outside handler will NOT see this click
});

Does not cancel default actions by itself. A click on <a> still navigates if you only stop propagation.

Both when you mean both

dropdownItem.addEventListener('click', (e) => {
  e.preventDefault();
  e.stopPropagation();
  select(item);
});

Capture vs bubble (why parent still runs)

parent.addEventListener('click', () => console.log('parent bubble'));
child.addEventListener('click', (e) => {
  e.stopPropagation();
  console.log('child');
});
// click child → child, parent does NOT run (bubble stopped)

parent.addEventListener('click', () => console.log('parent capture'), true);
// capture listener on parent runs BEFORE child — stopPropagation on child is too late for that capture

Order: capture (root → target) then bubble (target → root). stopPropagation prevents listeners on remaining nodes in the remaining phases.

stopImmediatePropagation

el.addEventListener('click', (e) => {
  e.stopImmediatePropagation();
});
el.addEventListener('click', () => {
  // never runs — same element, registered later
});

Stops other listeners on the same target too. Nuclear; use sparingly (libraries fighting each other).

Passive listeners and touch/wheel

window.addEventListener('touchmove', handler, { passive: true });
// handler calling preventDefault() is ignored (may warn)
// browser can scroll without waiting for JS

For custom pull-to-refresh or map pans that need preventDefault, register non-passive listeners (and accept jank risk if you run long tasks).

return false folklore

In inline handlers and jQuery, return false mixed preventDefault + stopPropagation. In addEventListener, return false does nothing useful. Call the methods explicitly.

Interview answer (out loud)

“preventDefault cancels the browser’s default action; stopPropagation stops the event from reaching other nodes. They’re independent. I know capture runs before target/bubble, stopImmediatePropagation blocks other listeners on the same node, and passive scroll/touch listeners can’t preventDefault.”

Keyboard defaults

input.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    e.preventDefault(); // avoid form submit
    submitForm();
  }
});

Space on buttons, arrow keys in custom listboxes, and Escape in dialogs all interact with default browser behavior — preventDefault is the tool; also implement ARIA patterns.

Delegation still sees the event

list.addEventListener('click', (e) => {
  const li = e.target.closest('li');
  if (!li) return;
  // child called stopPropagation → this may not run
});

If a child stops propagation, parent delegation fails. Prefer not stopping unless necessary; filter in the parent instead.

React synthetic events note

React 17+ attaches root listeners; stopPropagation on DOM still works with caveats across nested roots. Prefer explicit props (onClick on parent with target checks) over fighting propagation in React trees.

Further reading

Related guides