Command Palette

Search for a command to run...

Hectal
PHASE 9Intermediate ~21 min· topic 4 of 8

Topic 9.4

Default Actions

In one line

Many events come with a built-in browser behaviour, called the default action: a link navigates, a form sends and reloads the page, a checkbox toggles. event.preventDefault() cancels it so your JavaScript can do something else, and passive listeners promise never to cancel, which keeps scrolling smooth.

Think of it like this

A vending machine with a "cancel" button. When you press a snack's number, the machine's normal job is to drop that snack. That's the default action. If you press cancel in time, the machine doesn't drop it, but it still knows you pressed the number. preventDefault() is the cancel button: the event still happens and your listeners still hear it, but the browser skips its normal job.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Default action
What the browser does by itself after an event, like following a link or sending a form.
preventDefault()
The method that cancels the default action of an event.
cancelable
A property that says whether preventDefault() can cancel this event.
defaultPrevented
true once some listener has called preventDefault() on the event.
Submit
Sending a form's data, by default by loading a new page with it.
Passive listener
A listener added with { passive: true }, promising it will never call preventDefault().
Compositor thread
A separate part of the browser that can scroll and animate the page without waiting for JavaScript.

Step by step

01See the default action

A plain form with a submit button. Press Enter or click Send and the browser sends ?email=... to the action URL and loads a new page. Any JavaScript state on the old page is gone.

index.htmlwhole filehtml
<form id="signup" action="/subscribe">
  <input name="email" type="email" required>
  <button>Send</button>
</form>

02Listen for submit on the form

The listener goes on the form, for the submit event. It runs after the built-in checks (required, type="email") have passed, and before the browser sends anything.

app.jswhole filejavascript
const form = document.getElementById("signup");
form.addEventListener("submit", (event) => {
  // runs before the page would reload
});

03Cancel it

Call event.preventDefault() first, so even if later code throws, the page doesn't reload and hide the error.

app.jswhole filejavascript
form.addEventListener("submit", (event) => {
  event.preventDefault();
  console.log("Sending", form.elements.email.value, "with fetch instead");
});

04Where it fits in the event's life

The browser runs every listener in the path first. Only when dispatch is completely finished does it look at defaultPrevented and decide whether to act.

Where it fits in the event's lifediagram
Rendering diagram…

05Mark scroll listeners passive

A scroll-related listener that only reads values (to show a progress bar, for example) should say so. The browser can then scroll smoothly without waiting for it.

app.jswhole filejavascript
window.addEventListener("wheel", onWheel, { passive: true });
document.addEventListener("touchmove", onTouchMove, { passive: true });

Try it yourself

  1. 1

    Let the checkbox toggle

    In the first example, delete the checkbox's preventDefault(). Predict both checkbox lines, then run.

  2. 2

    Submit without JavaScript's help

    In the second example, comment out event.preventDefault();. Predict what the preview shows. Then put it back and add if (!query) { status.textContent = "Type something first"; return; } before the search line, and clear the box before submitting.

  3. 3

    Check before acting

    In the first example, add a second click listener on the link that logs event.defaultPrevented. Add it before, then after, the first one. Predict both results.

Code & diagrams

Links and checkboxes New tab

A checkbox is special: the browser flips `checked` before your listener runs, then flips it back if you cancel. So inside the listener, `checked` already shows the new value.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

link: navigation cancelled, defaultPrevented = true
checkbox during the listener: true
checkbox after the click: false
A form handled by JavaScript New tab

`submitter` is the button that sent the form; it's `null` here because code called `requestSubmit()` without naming one. Click the Search button yourself and it shows `<button>`. Delete the `preventDefault()` line and the preview reports a submitted form instead.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

submit handled in JS, query = debounce
submitter: null
still on the same page
What a passive listener can and can't do New tab

In the passive case Chrome also writes "Unable to preventDefault inside passive event listener invocation." to the browser's own DevTools console.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

listener ran, defaultPrevented = true
dispatchEvent returned false (false means cancelled)
listener ran, defaultPrevented = false
dispatchEvent returned true
input cancelable? false

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Listen for click on the submit button

Replace the form's submit listener with button.addEventListener("click", (e) => { e.preventDefault(); send(); }).

terminal
$ the page shows
── what you'll see ──
Empty required fields are sent without any warning, and some keyboard paths behave differently.

Break #2

preventDefault in a passive listener

Add document.addEventListener("touchmove", (e) => e.preventDefault()); to stop the page scrolling while drawing on a canvas.

terminal
$ DevTools console
── what you'll see ──
[Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive.

Myth vs fact

Myth

return false in a listener cancels the default action.

Fact

Only in on... handler properties and HTML attributes (onclick="return false"). In addEventListener listeners the return value is ignored. jQuery's handlers also treated return false as both preventDefault and stopPropagation, which is where the habit comes from.

Myth

preventDefault() stops other listeners from running.

Fact

It only cancels the browser's default action. The event keeps propagating and every listener still runs. Stopping listeners is stopPropagation()'s job.

Myth

Preventing default on keydown is a good way to stop paste or drop into a field.

Fact

Text can enter a field in many ways (paste, drag and drop, autofill, voice input). Use the beforeinput event, which covers all of them, or validate the value on input and on submit.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    For checkboxes and radio buttons, the HTML spec runs "legacy-pre-activation behaviour": the state flips before click listeners run and is restored if the event is cancelled. So checked inside the click listener shows the new state. Listen to change instead if you only care about the final state.

  • ▸

    dispatchEvent() returns false if the event was cancelable and some listener called preventDefault(). That's how custom components ask "may I?" before doing something (Topic 9.8).

  • ▸

    Passive listeners matter for the INP metric and for smooth scrolling. Chrome's DevTools Performance panel flags non-passive touch and wheel listeners that delayed scrolling. addEventListener with an options object is supported everywhere current; if you must support very old browsers, feature-detect passive support before passing { passive: true }.

  • ▸

    form.submit() (no "request") skips validation and does not fire the submit event, so your listener never runs. Use form.requestSubmit(button?) to submit from code like a real button press.

Remember this

  1. 1

    A default action is what the browser does by itself in response to an event. Clicking an <a href> navigates to the link. Pressing a submit button submits the form: the browser sends the data and loads a new page. Clicking a checkbox toggles it. Pressing a key in a text box types a character. Right-clicking opens the context menu. Pressing Space on the page scrolls. Your listeners run first; the browser does the default action after dispatch finishes, unless someone cancelled it.

  2. 2

    event.preventDefault() cancels the default action. The event still propagates normally (bubbling is unaffected), other listeners still run, and event.defaultPrevented becomes true so later listeners can check it. It only works if event.cancelable is true. Some events can't be cancelled because the thing already happened: input (the text has already changed), scroll, focus, load. For those, calling preventDefault() silently does nothing.

  3. 3

    The most common use is a form handled by JavaScript. Without preventDefault(), submitting reloads the page and your JavaScript state is lost. With it, you read the fields yourself (Topic 9.6), validate, and send them with fetch (Topic 11.2). Always listen for the form's **submit** event, not the button's click: submit also fires when the user presses Enter in a field, and only fires after the browser's built-in validation passes.

  4. 4

    Other uses: stop a link from navigating in a single-page app so a router can show the page instead (Topic 11.5); stop a checkbox from toggling until the server confirms; block a key such as letters in a number-only box (keydown + preventDefault()); show your own menu on contextmenu; or allow dropping files (dragover must be cancelled before drop fires). Each of these takes away something users expect, so replace the behaviour, don't just remove it.

  5. 5

    A passive listener is one added with { passive: true }. It promises the browser it will never call preventDefault(). Why does that matter? For touchstart, touchmove and wheel, the default action is scrolling. Normally the browser must wait for your listener to finish, in case it cancels the scroll, before it can move the page. With a passive listener, it scrolls straight away on its own compositor thread, while your code runs on the main thread. If a passive listener calls preventDefault() anyway, it's ignored and Chrome prints a console warning.

  6. 6

    Browsers already treat touchstart, touchmove, wheel and mousewheel listeners added to window, document or document.body as passive by default (an intervention from 2017 onwards). If you really need to block scrolling there (a custom drawing canvas, a map), add { passive: false } explicitly, or better, use the CSS property touch-action: none on the element so the browser knows without asking JavaScript.

Explain it without notes

01

What is a default action? Give three examples.

02

Why should you handle a form's submit event rather than the submit button's click?

03

What is a passive event listener and why do browsers want them?

04

What is the difference between preventDefault() and stopPropagation()?

Practice

01

Make a link <a href="/delete" id="remove">Delete</a> log Are you sure? instead of navigating. Click it from code.

02

Make a text input #digits that refuses any key that isn't a digit, Backspace, Tab or an arrow key. Test it by dispatching a keydown for a and for 5 and logging whether each was cancelled.

Trade-offs

  • ↔

    Handling a form in JavaScript allows inline errors and no page reload, but you must rebuild what the browser gave for free: loading states, error handling and a working page if JavaScript fails (progressive enhancement keeps a real action URL).

  • ↔

    Cancelling defaults gives full control but removes behaviour users rely on (middle-click to open links in a new tab, right-click menus); only cancel when you replace the behaviour with something at least as good.

Done when you can

  • I can name common default actions and cancel them with preventDefault().

  • I handle forms with the submit event on the <form>, not the button's click.

  • I can check cancelable and defaultPrevented.

  • I can explain passive listeners and when to use { passive: true }.

  • I know preventDefault and stopPropagation are independent.