Skip to content
JavaScript js events 5 min read

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 / eventDefault action
<a> clickNavigate to the href
<form> submitSend the request and reload/navigate
contextmenuShow the right-click menu
<input type="checkbox"> clickToggle the checked state
dragover / dropReject the drop (must be prevented to allow it)
keydown on a form fieldInsert 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 required field is empty, the submit event never fires, so your preventDefault never executes — the browser blocks submission and shows its own message. Call form.checkValidity() if you need to test it manually.

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.

MethodStops the default action?Stops propagation?
preventDefault()YesNo
stopPropagation()NoYes
stopImmediatePropagation()NoYes, 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 false from an inline onclick="..." attribute also cancels the default, but it is legacy and only works in that one context. With addEventListener, the return value is ignored — always call event.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 on defaultPrevented afterward) when behavior depends on whether cancellation succeeded.
  • Keep preventDefault and stopPropagation distinct in your mind — reach for the one that matches the problem.
  • Never add { passive: true } to a listener that calls preventDefault; the browser will ignore the cancellation and warn in the console.
  • For drag-and-drop, remember you must prevent the default on dragover and drop, or the drop never works.
  • Let native form validation run; gate your fetch logic behind a submit handler that calls preventDefault first.
Last updated June 1, 2026
Was this helpful?