Skip to main content

How to Control Browser Default Actions in JavaScript

When you click a link, the browser navigates to a new page. When you submit a form, the browser sends the data to the server and reloads. When you right-click, a context menu appears. When you press a key in a text field, a character appears. These are all default actions: built-in behaviors that the browser performs automatically in response to events.

In modern web development, you frequently need to override these defaults. You want a link to trigger a JavaScript function instead of navigating. You want a form to submit via AJAX instead of reloading the page. You want to build a custom context menu instead of the browser's default one. JavaScript gives you event.preventDefault() to stop these default behaviors and implement your own.

This guide covers what default actions exist, how to prevent them, the important nuances of passive listeners, and how to check whether a default was already prevented by another handler.

Common Default Actions

Almost every user interaction has a browser default action associated with it. Here are the ones you will encounter most frequently:

Clicking an <a> element navigates to the URL in its href attribute:

<a href="https://example.com">Visit Example</a>
<a href="#section-2">Jump to Section 2</a>
<a href="/dashboard">Go to Dashboard</a>

Clicking any of these triggers navigation, hash scrolling, or a page load by default.

Form Submission

Submitting a <form> sends the form data to the URL specified in action and reloads the page:

<form action="/api/login" method="POST">
<input type="text" name="username">
<input type="password" name="password">
<button type="submit">Log In</button>
</form>

Pressing Enter inside an input or clicking the submit button triggers the submit event and causes a full page reload.

Text Selection and Drag

Pressing the mouse button and dragging over text selects it. Dragging an image starts a native drag-and-drop operation:

<p>Try selecting this text by clicking and dragging.</p>
<img src="photo.jpg" alt="Draggable image">

Keyboard Input

Typing in a text field inserts characters. Pressing Tab moves focus. Pressing Escape may close dialogs. Pressing Space scrolls the page or activates buttons:

<input type="text" placeholder="Type here">
<textarea>Editable text area</textarea>

Context Menu

Right-clicking (or Ctrl+click on Mac) opens the browser's context menu:

<div id="canvas">Right-click for options</div>

Checkbox and Radio Toggle

Clicking a checkbox toggles its checked state. Clicking a radio button selects it and deselects siblings:

<input type="checkbox" id="agree"> Agree to terms
<input type="radio" name="size" value="small"> Small
<input type="radio" name="size" value="large"> Large

Other Common Defaults

EventDefault Action
mousedown on textStarts text selection
mousedown on imageStarts drag operation
keydown (Tab)Moves focus to next focusable element
keydown (Space) on buttonActivates the button (triggers click)
keydown (Space) on pageScrolls down
keydown (Arrow keys)Scrolls or moves cursor
wheelScrolls the page
touchmoveScrolls on mobile
dragstartBegins native drag-and-drop
click on <input type="submit">Submits the parent form

Preventing Defaults: event.preventDefault()

The event.preventDefault() method tells the browser: "Do not perform the default action for this event." The event still fires, handlers still run, and the event still bubbles, but the browser's built-in response is cancelled.

<a href="https://example.com" id="custom-link">Click Me</a>
document.getElementById("custom-link").addEventListener("click", (event) => {
event.preventDefault(); // Browser will NOT navigate to example.com

console.log("Link clicked, but navigation prevented");
loadContentViaAjax(event.currentTarget.href);
});

The click event fires normally. The handler runs. But the browser does not navigate away. This is the foundation of client-side routing in single-page applications.

Preventing Form Submission

<form id="login-form" action="/api/login" method="POST">
<input type="text" name="username" required>
<input type="password" name="password" required>
<button type="submit">Log In</button>
</form>
document.getElementById("login-form").addEventListener("submit", (event) => {
event.preventDefault(); // No page reload, no server submission

const formData = new FormData(event.target);
const username = formData.get("username");
const password = formData.get("password");

// Submit via fetch instead
fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password })
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.href = "/dashboard";
} else {
showError("Invalid credentials");
}
})
.catch(err => showError("Network error"));
});

This is the most common use of preventDefault() in modern applications. You take control of form submission to provide better UX: no page reload, inline error messages, loading states, and asynchronous processing.

Custom Context Menu

document.getElementById("canvas").addEventListener("contextmenu", (event) => {
event.preventDefault(); // Suppress the native right-click menu

showCustomContextMenu(event.clientX, event.clientY, [
{ label: "Copy", action: handleCopy },
{ label: "Paste", action: handlePaste },
{ label: "Delete", action: handleDelete }
]);
});

Preventing Text Selection

Useful for UI elements that should not be selectable, like buttons or drag handles:

document.getElementById("toolbar").addEventListener("mousedown", (event) => {
event.preventDefault(); // Prevents text selection on double-click or drag
});
// Prevent selection during drag operations
element.addEventListener("mousedown", (event) => {
event.preventDefault(); // No text selection while dragging

const onMouseMove = (e) => {
element.style.left = e.clientX + "px";
element.style.top = e.clientY + "px";
};

const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};

document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});

Preventing Keyboard Defaults

// Custom keyboard shortcuts
document.addEventListener("keydown", (event) => {
// Ctrl+S or Cmd+S: save document instead of browser save dialog
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
event.preventDefault();
saveDocument();
}

// Ctrl+K: open custom search instead of browser address bar
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
event.preventDefault();
openSearchDialog();
}
});
// Number-only input: prevent non-numeric characters
document.getElementById("phone").addEventListener("keydown", (event) => {
const allowed = [
"Backspace", "Delete", "Tab", "ArrowLeft", "ArrowRight",
"Home", "End"
];

if (allowed.includes(event.key)) return; // Allow navigation keys
if (event.ctrlKey || event.metaKey) return; // Allow Ctrl+C, Ctrl+V, etc.

if (!/^\d$/.test(event.key)) {
event.preventDefault(); // Block non-digit characters
}
});
warning

Be careful when preventing keyboard defaults. Blocking keys like Tab, Escape, or Ctrl+C breaks accessibility and expected user interactions. Only prevent defaults when you are providing a clear, discoverable alternative behavior.

Preventing Checkbox Toggle

document.getElementById("terms-checkbox").addEventListener("click", (event) => {
if (!userHasReadTerms()) {
event.preventDefault(); // Checkbox state does NOT change
alert("Please read the terms first");
}
// If userHasReadTerms() returns true, default action proceeds normally
});

Multiple Event Types on One Element

Some interactions involve multiple events, and you may need to prevent defaults on more than one:

const dragHandle = document.getElementById("drag-handle");

// Prevent both default actions for clean drag behavior
dragHandle.addEventListener("mousedown", (event) => {
event.preventDefault(); // Prevent text selection
});

dragHandle.addEventListener("dragstart", (event) => {
event.preventDefault(); // Prevent native drag-and-drop (images, links)
});

Not All Defaults Can Be Prevented

Some defaults are not cancellable. You can check whether an event's default can be prevented by reading event.cancelable:

window.addEventListener("scroll", (event) => {
console.log("Cancelable:", event.cancelable); // false for scroll events
event.preventDefault(); // Has no effect: scroll is not cancelable here
});

document.addEventListener("click", (event) => {
console.log("Cancelable:", event.cancelable); // true
event.preventDefault(); // Works
});

preventDefault() Does Not Stop Propagation

This is an important distinction. preventDefault() cancels the browser's default action but does not stop the event from bubbling:

const link = document.getElementById("my-link");
const container = document.getElementById("container");

link.addEventListener("click", (event) => {
event.preventDefault(); // No navigation
console.log("Link handler ran");
// Event still bubbles to container
});

container.addEventListener("click", (event) => {
console.log("Container handler ran"); // This STILL fires
});

// Output when clicking the link:
// "Link handler ran"
// "Container handler ran"

To stop both the default action and propagation, you need both calls:

link.addEventListener("click", (event) => {
event.preventDefault(); // No navigation
event.stopPropagation(); // No bubbling
});

Passive Listeners and preventDefault()

Modern browsers introduced passive event listeners as a performance optimization, particularly for scroll and touch events. A passive listener promises the browser that it will never call preventDefault(), allowing the browser to proceed with its default action immediately without waiting for the handler to finish.

The Performance Problem

When you add a touchmove or wheel listener, the browser cannot start scrolling until your handler finishes executing, because it does not know whether your handler will call preventDefault(). Even if your handler never prevents scrolling, the browser must wait:

// Without passive: browser waits for handler before scrolling
document.addEventListener("touchmove", (event) => {
// Even this empty handler delays scroll on mobile
trackTouchPosition(event);
});

This causes visible scroll jank, especially on mobile devices where smooth scrolling is essential.

Passive Listeners: The Solution

By declaring a listener as passive, you tell the browser: "This handler will never cancel the default action. Scroll immediately."

// With passive: browser scrolls immediately, handler runs in parallel
document.addEventListener("touchmove", (event) => {
trackTouchPosition(event);
}, { passive: true });

What Happens If You Try preventDefault() in a Passive Listener

Calling preventDefault() inside a passive listener has no effect and triggers a console warning:

document.addEventListener("wheel", (event) => {
event.preventDefault(); // WARNING and no effect!
// Console: "Unable to preventDefault inside passive event listener invocation."
}, { passive: true });

The browser ignores the preventDefault() call because you promised not to use it.

Browser Default Behavior for passive

Modern browsers (Chrome 56+, Firefox 61+) automatically make touchstart, touchmove, wheel, and related event listeners passive by default when registered on document, window, document.body, or document.documentElement:

// These are automatically passive in modern browsers:
document.addEventListener("touchmove", handler); // Implicitly passive
window.addEventListener("wheel", handler); // Implicitly passive

// These are NOT automatically passive:
someDiv.addEventListener("touchmove", handler); // Not passive by default
someDiv.addEventListener("wheel", handler); // Not passive by default

Explicitly Non-Passive for Scroll Prevention

If you genuinely need to prevent scrolling (for example, in a custom scroll container or a drawing canvas), you must explicitly set passive: false:

// Canvas drawing: must prevent touch scroll
canvas.addEventListener("touchmove", (event) => {
event.preventDefault(); // Actually works because passive is false
drawLine(event.touches[0]);
}, { passive: false }); // Explicitly opt out of passive

// Custom scroll behavior: need to prevent default wheel
scrollContainer.addEventListener("wheel", (event) => {
event.preventDefault();
customScroll(event.deltaY);
}, { passive: false });

Combining Passive with Other Options

element.addEventListener("touchmove", handler, {
passive: true, // Performance optimization
capture: false, // Bubble phase (default)
once: false // Keep listening (default)
});

element.addEventListener("wheel", handler, {
passive: false, // Need preventDefault
capture: true, // Capture phase
once: true // Remove after first call
});

Checking If Passive Is Supported

In very old browsers that do not support the options object, passing an object as the third argument could be misinterpreted as useCapture = true. If you need to support legacy browsers:

let passiveSupported = false;

try {
const options = {
get passive() {
passiveSupported = true;
return false;
}
};
window.addEventListener("test", null, options);
window.removeEventListener("test", null, options);
} catch (err) {
passiveSupported = false;
}

// Use the detection
element.addEventListener(
"touchmove",
handler,
passiveSupported ? { passive: true } : false
);
info

The passive listener optimization matters most for touchmove, touchstart, and wheel events, where scroll performance is critical. For other events like click, keydown, or submit, passive has no practical impact because the browser does not need to wait for those handlers to decide whether to scroll.

event.defaultPrevented: Checking If Default Was Prevented

The event.defaultPrevented property is a boolean that tells you whether preventDefault() has already been called on this event by a previous handler. This is useful in event delegation and when multiple handlers on different elements need to coordinate.

Basic Usage

document.getElementById("link").addEventListener("click", (event) => {
console.log("Before:", event.defaultPrevented); // false

event.preventDefault();

console.log("After:", event.defaultPrevented); // true
});

Use Case: Coordinating Between Handlers

When using event delegation, a child handler might prevent the default, and the parent handler needs to know:

// Child handler: prevent navigation for specific links
document.getElementById("internal-link").addEventListener("click", (event) => {
event.preventDefault();
loadPageViaAjax(event.currentTarget.href);
});

// Parent handler: only act if the default wasn't already handled
document.getElementById("nav").addEventListener("click", (event) => {
if (event.defaultPrevented) {
// A child handler already dealt with this event
console.log("Already handled by a child");
return;
}

// Default was not prevented: handle it here
trackNavigation(event.target);
});

Use Case: Alternative to stopPropagation()

defaultPrevented provides a way to signal between handlers without stopping propagation. This is often better than stopPropagation() because all handlers still fire and can inspect the event:

// Instead of stopPropagation (which blocks ALL parent handlers):
child.addEventListener("click", (event) => {
event.stopPropagation(); // Parent analytics handler won't fire!
handleChildClick();
});

// Use preventDefault as a signal (parent handlers still fire):
child.addEventListener("click", (event) => {
event.preventDefault(); // Signal that we handled it
handleChildClick();
});

// Parent checks the signal
parent.addEventListener("click", (event) => {
// Analytics still runs
trackClick(event.target);

// But functional handler respects the signal
if (!event.defaultPrevented) {
handleParentClick();
}
});

Use Case: Custom Context Menu with Fallback

// Component provides a custom context menu
component.addEventListener("contextmenu", (event) => {
event.preventDefault();
showCustomMenu(event.clientX, event.clientY);
});

// Global handler: show default menu only if no custom menu was shown
document.addEventListener("contextmenu", (event) => {
if (event.defaultPrevented) {
return; // A component already handled this
}

// No custom handler: let the browser show its default menu
// (We don't prevent the default here, so the native menu appears)
});

Use Case: Form Validation Chain

const form = document.getElementById("user-form");

// First handler: field-level validation
form.addEventListener("submit", (event) => {
const email = form.elements.email.value;

if (!email.includes("@")) {
event.preventDefault();
showFieldError("email", "Invalid email address");
}
});

// Second handler: business logic validation (only if fields are valid)
form.addEventListener("submit", (event) => {
if (event.defaultPrevented) {
return; // Field validation already failed
}

const email = form.elements.email.value;
if (bannedDomains.includes(email.split("@")[1])) {
event.preventDefault();
showFieldError("email", "This email domain is not allowed");
}
});

// Third handler: submit handler (only if all validation passed)
form.addEventListener("submit", (event) => {
if (event.defaultPrevented) {
return; // Validation failed somewhere
}

event.preventDefault(); // Prevent normal form submission
submitViaAjax(form); // Submit via AJAX instead
});

Each handler checks event.defaultPrevented to see if a previous handler already rejected the submission. This creates a clean validation pipeline without requiring the handlers to know about each other.

return false in HTML Attribute Handlers

In inline HTML event handlers (onclick="...", onsubmit="..."), returning false has a special effect: it prevents the default action. This is a legacy behavior that works only in HTML attribute handlers, not in addEventListener.

How It Works in HTML Attributes

<!-- return false prevents navigation -->
<a href="https://example.com" onclick="console.log('clicked'); return false;">
Click Me (no navigation)
</a>

<!-- return false prevents form submission -->
<form onsubmit="return validateForm();">
<input type="text" name="name" required>
<button type="submit">Submit</button>
</form>

<script>
function validateForm() {
const name = document.querySelector('[name="name"]').value;
if (name.length < 2) {
alert("Name too short");
return false; // Prevents form submission
}
return true; // Allows form submission
}
</script>

return false Does NOT Work with addEventListener

This is a common source of confusion. In addEventListener handlers, returning false has no effect on the default action:

// DOES NOT prevent navigation: return false is ignored
document.getElementById("link").addEventListener("click", (event) => {
console.log("Clicked");
return false; // This does NOTHING in addEventListener
});

// CORRECT: use preventDefault()
document.getElementById("link").addEventListener("click", (event) => {
event.preventDefault(); // This actually prevents navigation
console.log("Clicked");
});

Why You Should Avoid return false in HTML Attributes

There are several reasons to prefer addEventListener with preventDefault() over inline handlers with return false:

<!-- BAD: Inline handler with return false -->
<a href="/page" onclick="handleClick(); return false;">Link</a>

<!-- Problems:
1. Mixes HTML and JavaScript
2. If handleClick() throws, return false never executes → navigation happens
3. Limited to one handler per event
4. Hard to debug
5. Cannot control propagation
-->
// GOOD: addEventListener with preventDefault
document.querySelector("a").addEventListener("click", (event) => {
event.preventDefault(); // Always runs, even if handleClick throws
handleClick();
});

// Benefits:
// 1. Separation of concerns
// 2. preventDefault() runs before potentially throwing code
// 3. Multiple handlers possible
// 4. Easy to debug
// 5. Full control over propagation

The jQuery Legacy

In jQuery, return false in event handlers calls both event.preventDefault() and event.stopPropagation(). This is jQuery-specific behavior and does not apply to vanilla JavaScript:

// jQuery: return false = preventDefault + stopPropagation
$("a").on("click", function() {
// ... handle click ...
return false; // Prevents default AND stops propagation
});

// Vanilla JS: return false does NOTHING
document.querySelector("a").addEventListener("click", function() {
return false; // No effect at all
});

// Vanilla JS equivalent of jQuery's return false:
document.querySelector("a").addEventListener("click", function(event) {
event.preventDefault();
event.stopPropagation();
});
caution

If you are transitioning from jQuery to vanilla JavaScript, remember that return false loses its special meaning. Always use event.preventDefault() and event.stopPropagation() explicitly.

Summary of return false Behavior

ContextEffect of return false
HTML attribute (onclick="return false")Prevents default action
addEventListener callbackNo effect
jQuery .on() handlerPrevents default AND stops propagation
window.onerror handlerSuppresses the error in the console

Summary

Browser default actions are the built-in responses to user interactions. Controlling them with preventDefault() is essential for building modern interactive applications where you need custom behavior instead of the browser's defaults.

ConceptKey Point
Default actionsBrowser's built-in responses to events: navigation, form submission, scrolling, text selection, etc.
event.preventDefault()Cancels the default action. The event still fires and bubbles normally.
event.cancelableBoolean indicating whether preventDefault() will have any effect.
Passive listenersPromise the browser that preventDefault() will never be called, enabling performance optimizations.
{ passive: true }Explicitly mark a listener as passive. preventDefault() calls are ignored with a warning.
{ passive: false }Explicitly mark a listener as non-passive. Required when you need to prevent scroll/touch defaults.
Auto-passivetouchstart, touchmove, wheel on document/window are passive by default in modern browsers.
event.defaultPreventedBoolean indicating whether preventDefault() was already called by a previous handler.
return false in HTMLPrevents default in inline handlers (onclick="return false"). Does NOT work with addEventListener.
preventDefault vs stopPropagationpreventDefault cancels the browser's action. stopPropagation stops the event from reaching other elements. They are independent.

Key rules to remember:

  • preventDefault() does not stop propagation. Use stopPropagation() separately if needed.
  • Always use event.preventDefault() in addEventListener handlers. return false only works in HTML attribute handlers.
  • Use { passive: true } for scroll and touch handlers when you do not need to prevent scrolling. Modern browsers make this the default on document-level listeners.
  • Use { passive: false } explicitly when you genuinely need to prevent scroll or touch defaults (custom scroll containers, drawing canvases).
  • Check event.defaultPrevented in parent handlers to coordinate with child handlers without using stopPropagation().
  • Check event.cancelable before calling preventDefault() to avoid confusion with non-cancelable events.
  • Be cautious about preventing keyboard and accessibility-related defaults. Users expect Tab, Escape, and standard shortcuts to work.