preventDefault and stopPropagation
Every event has built-in browser behaviour attached to it — links navigate, forms submit, right-clicks open menus, checkboxes toggle. Sometimes you want that behaviour, sometimes you want to replace it with your own. Two related-but-different methods control this: preventDefault() cancels the default action; stopPropagation() cancels further travel of the event. Mixing them up is a classic source of "why doesn't my link work?" bugs.
preventDefault — cancel the browser's reaction
Many events have a default action baked into the browser. When the event reaches the target, the browser asks: "did anybody call preventDefault?" If not, it carries out that default. Calling event.preventDefault() flips the flag.
Click on
<a href>→ navigate.preventDefaultstops the navigation.submiton<form>→ POST and reload.preventDefaultlets you handle the data in JavaScript instead.contextmenu(right-click) → show the OS menu.preventDefaultsuppresses it.keydown→ type the character / scroll the page.preventDefaultblocks that key.dragstart,drop→ default file handling.preventDefaultlets you customise it.
Cancelling a link click
<a id="docs" href="/docs">Open docs</a>
document.getElementById("docs").addEventListener("click", (e) => {
e.preventDefault(); // do not navigate
openInModal("/docs"); // do this instead
});Submitting a form with fetch
<form id="signup"> <input name="email" type="email" required /> <button>Sign up</button> </form>
document.getElementById("signup").addEventListener("submit", async (e) => {
e.preventDefault(); // skip the page reload
const data = new FormData(e.currentTarget);
await fetch("/api/signup", { method: "POST", body: data });
});Without that preventDefault the browser would do a classic full-page submit. With it, you keep control and stay in your single-page app.
Suppressing the right-click menu
canvas.addEventListener("contextmenu", (e) => e.preventDefault());stopPropagation — do not let it travel further
stopPropagation() is unrelated to defaults — it tells the event "stop walking the DOM after this listener". Listeners further up (or further down in capture phase) will not see it.
document.addEventListener("click", () => closeMenu());
menu.addEventListener("click", (e) => {
e.stopPropagation(); // click inside the menu does not bubble to document
});This is how the classic "click outside to close" pattern works without using a flag.
stopImmediatePropagation — stop sibling listeners too
stopImmediatePropagation() is the strictest of the three. It stops other listeners on the same element from running as well as preventing the event from propagating up or down.
btn.addEventListener("click", (e) => {
e.stopImmediatePropagation();
console.log("only this one runs");
});
btn.addEventListener("click", () => console.log("never runs"));preventDefault vs stopPropagation — they are independent
Returning to the link example: calling stopPropagation will not stop the navigation. The navigation is the default action, which lives in the browser's normal handling of the click — it is unrelated to whether other listeners see the event. To stop a link, you need preventDefault.
bug: navigates anyway
link.addEventListener("click", (e) => {
e.stopPropagation(); // does not cancel navigation
doSomething();
});fix: cancel the default
link.addEventListener("click", (e) => {
e.preventDefault();
doSomething();
});Passive listeners cannot preventDefault
Which one do you actually need?
Stop a link from navigating, a form from submitting, a key from typing →
preventDefault.Stop a parent listener from also handling the same click →
stopPropagation.Stop other handlers on the same element from running →
stopImmediatePropagation.Need both? Call both. They are independent.