preventDefault & Default Actions
Many events carry a built-in default action the browser performs unless you intervene: clicking a link navigates, submitting a form reloads the page, right-clicking opens the context menu. The event.preventDefault() method cancels that built-in behavior so your own JavaScript can take over. Mastering it is what turns plain HTML into a real single-page experience — intercepting form submits, building custom shortcuts, and controlling drag-and-drop.
What a default action is
A default action is the browser’s native response to an event, defined by the HTML/DOM spec rather than your code. The handler still runs, but afterward the browser does its thing too — unless you cancel it. Common defaults include:
| Element / event | Default action |
|---|---|
<a> click | Navigate to the href |
<form> submit | Send the request and reload/navigate |
contextmenu | Show the right-click menu |
<input type="checkbox"> click | Toggle the checked state |
dragover / drop | Reject the drop (must be prevented to allow it) |
keydown on a form field | Insert the typed character |
Only cancelable events have a default to stop. You can check event.cancelable — calling preventDefault() on a non-cancelable event is silently ignored.
Calling preventDefault
preventDefault() takes no arguments. Call it from inside a handler on a cancelable event, and the browser skips the native behavior. It does not stop the event from propagating — bubbling and capturing continue normally (that is what stopPropagation is for).
const link = document.querySelector("#help");
link.addEventListener("click", (event) => {
event.preventDefault(); // browser will NOT follow the href
console.log("intercepted, cancelable:", event.cancelable);
// Your own logic: open a modal, route client-side, etc.
});
Output:
intercepted, cancelable: true
After the handler finishes you can also inspect event.defaultPrevented to confirm the action was cancelled — useful when several handlers cooperate.
Intercepting form submits
The classic use case: capture a form submit, cancel the page reload, and handle the data with fetch. Read values from the form, then send them asynchronously.
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: system-ui, sans-serif; padding: 24px; }
input, button { font-size: 1rem; padding: 8px; margin: 4px 0; display: block; }
#out { margin-top: 12px; font-family: monospace; }
</style>
</head>
<body>
<form id="signup">
<input name="email" type="email" placeholder="[email protected]" required />
<button type="submit">Sign up</button>
</form>
<div id="out"></div>
<script>
const form = document.querySelector("#signup");
const out = document.querySelector("#out");
form.addEventListener("submit", (event) => {
event.preventDefault(); // no full-page reload
const data = new FormData(form);
const email = data.get("email");
out.textContent = `Submitting ${email} via fetch (page stayed put)`;
// await fetch("/api/signup", { method: "POST", body: data });
});
</script>
</body>
</html>
Browser-native validation still runs first. If a
requiredfield is empty, thesubmitevent never fires, so yourpreventDefaultnever executes — the browser blocks submission and shows its own message. Callform.checkValidity()if you need to test it manually.
Links, the context menu, and drag
Cancelling navigation lets you build client-side routers or confirmation prompts. Suppressing the context menu is common for custom right-click menus. And drag-and-drop requires prevention: by default the browser refuses drops, so you must cancel both dragover and drop.
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: system-ui, sans-serif; padding: 24px; }
#drop { border: 2px dashed #888; padding: 30px; text-align: center; border-radius: 8px; }
#drop.over { border-color: #2563eb; background: #eff6ff; }
#log { margin-top: 12px; font-family: monospace; white-space: pre-line; }
</style>
</head>
<body>
<a id="route" href="https://example.com">Client-side link (won't navigate)</a>
<div id="drop" draggable="false">Drop a file here</div>
<div id="log"></div>
<script>
const log = document.querySelector("#log");
const print = (m) => (log.textContent += m + "\n");
// 1. Cancel link navigation
document.querySelector("#route").addEventListener("click", (e) => {
e.preventDefault();
print("link click intercepted, no navigation");
});
// 2. Replace the context menu
document.querySelector("#drop").addEventListener("contextmenu", (e) => {
e.preventDefault();
print("custom menu would open here");
});
// 3. Enable a drop zone (defaults must be cancelled)
const zone = document.querySelector("#drop");
zone.addEventListener("dragover", (e) => {
e.preventDefault(); // required to allow a drop
zone.classList.add("over");
});
zone.addEventListener("dragleave", () => zone.classList.remove("over"));
zone.addEventListener("drop", (e) => {
e.preventDefault(); // stop the browser opening the file
zone.classList.remove("over");
const name = e.dataTransfer.files[0]?.name ?? "(no file)";
print("dropped: " + name);
});
</script>
</body>
</html>
Keyboard shortcuts
For custom hotkeys, prevent the browser’s default so the keystroke does not trigger built-in behavior (like Ctrl+S opening the save dialog). Inspect the modifier flags on the KeyboardEvent, then cancel.
document.addEventListener("keydown", (event) => {
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
event.preventDefault(); // stop the browser save dialog
saveDocument();
}
});
preventDefault vs stopPropagation
These solve different problems and are often confused. preventDefault cancels the browser’s native action; stopPropagation halts the event’s travel through the DOM tree. They are independent — you can use either, both, or neither.
| Method | Stops the default action? | Stops propagation? |
|---|---|---|
preventDefault() | Yes | No |
stopPropagation() | No | Yes |
stopImmediatePropagation() | No | Yes, plus other handlers on the same element |
When you genuinely need both — say, an in-list link that must neither navigate nor trigger the parent’s delegated click handler — call them together.
item.addEventListener("click", (event) => {
event.preventDefault(); // don't follow the href
event.stopPropagation(); // don't bubble to the list container
selectItem(item);
});
Returning
falsefrom an inlineonclick="..."attribute also cancels the default, but it is legacy and only works in that one context. WithaddEventListener, the return value is ignored — always callevent.preventDefault()explicitly.
Best Practices
- Call
event.preventDefault()only when you are replacing the native behavior, not reflexively on every handler. - Check
event.cancelable(or rely ondefaultPreventedafterward) when behavior depends on whether cancellation succeeded. - Keep
preventDefaultandstopPropagationdistinct in your mind — reach for the one that matches the problem. - Never add
{ passive: true }to a listener that callspreventDefault; the browser will ignore the cancellation and warn in the console. - For drag-and-drop, remember you must prevent the default on
dragoveranddrop, or the drop never works. - Let native form validation run; gate your
fetchlogic behind asubmithandler that callspreventDefaultfirst.