Browser default actions
The browser does a lot of work for you without being asked. Click a link and it navigates. Click a submit button and the form goes to the server. Drag across some text and it gets selected. Each of these is a default action: a built-in reaction the browser performs when a specific event fires.
Most of the time that’s exactly what you want. But when you handle an event yourself in JavaScript, the built-in reaction often gets in your way. You wrote code to display a route in a single-page app, and now the browser also reloads the page out from under you. To stop that, you have to tell the browser: I’ve got this, stand down.
<a>Preventing browser actions
You have two levers to stop a default action.
- The main one is the event object. Call
event.preventDefault()and the browser skips its built-in reaction. - A shortcut exists when the handler is assigned through an
on<event>property (likeelem.onclickor anonclick="..."attribute, notaddEventListener). In that case,return falsefrom the handler does the same job.
Both links below refuse to navigate — clicking them does nothing:
<a href="/" onclick="return false">Click here</a>
or
<a href="/" onclick="event.preventDefault()">here</a>
We’ll put this to work in a moment to build a menu that runs on JavaScript instead of page loads.
Example: the menu
Here’s a small site menu:
<ul id="menu" class="menu">
<li><a href="/ferns">Ferns</a></li>
<li><a href="/succulents">Succulents</a></li>
<li><a href="/orchids">Orchids</a></li>
</ul>
Styled up and wired to JavaScript, it behaves like this — click an item and the content swaps in place instead of the page navigating away:
Notice the items are real <a href="..."> links, not <button> or <span> elements. That’s a deliberate choice, and it matters:
- People expect to right-click a menu item and pick “open in a new tab”. That only works on a real link. A
<button>or<span>gives them nothing. - Search engine crawlers follow
<a href="...">while indexing your site. Buttons are invisible to that process.
So the markup stays semantic with <a>. But we still want JavaScript to take over the click — maybe to load content over the network and render it in place. That means canceling the navigation the browser would otherwise perform:
menu.onclick = function(event) {
if (event.target.nodeName != 'A') return;
let href = event.target.getAttribute('href');
alert( href ); // ...could kick off a fetch, build UI, etc.
return false; // cancel the default action (don't follow the URL)
};
Drop that return false and the sequence changes. Your code runs, and then the browser proceeds with its default: navigating to whatever href holds, which reloads the page. Since you’re handling the click yourself, you don’t want that second step.
(page reloads)
stays on page
One bonus of handling the click on the <ul> and reading event.target: this is event delegation, and it keeps the menu flexible. You can add nested lists later and animate them with CSS to “slide down”, without touching the click logic.
Try it yourself. Clicking the first field focuses it and clears the placeholder text; clicking the second does nothing, because its focus-on-mousedown was canceled — but Tab into it still works:
The “passive” handler option
addEventListener takes an optional passive: true flag. It’s a promise you make to the browser: this handler will never call preventDefault().
Why would the browser care?
Think about touchmove on a phone — the user dragging a finger across the screen. By default that scrolls the page. But a handler can cancel scrolling with preventDefault(), maybe to build a custom swipe gesture. So the browser is stuck waiting: it can’t scroll until every touchmove handler has run and it’s confirmed that none of them called preventDefault(). Only then does it know scrolling is allowed.
That wait shows up as lag and jitter while the user is actively trying to scroll — the worst possible moment for the UI to hesitate.
With passive: true, the browser stops waiting. It scrolls at once for a smooth, fluid feel, and your handler runs alongside.
For a couple of touch events — touchstart and touchmove — browsers like Firefox and Chrome already treat listeners on document-level targets as passive: true by default, precisely because scroll performance matters so much there.
event.defaultPrevented
event.defaultPrevented is a boolean that reads back whether the default action was canceled: true if some handler called preventDefault(), false if not. It’s a way to ask, after the fact, “did anyone already handle this?”
That question turns out to be genuinely useful.
Remember from Bubbling and capturing that event.stopPropagation() halts bubbling, and that killing bubbling is usually a bad idea — it silently cuts off every outer handler, including ones you don’t own. In some cases event.defaultPrevented gives you a cleaner alternative: instead of stopping the event, you let it keep bubbling but leave a flag saying “already dealt with.”
Here’s the problem it solves.
By default, a right-click fires contextmenu and the browser opens its own menu. You can replace that with your own:
<button>Right-click shows browser context menu</button>
<button oncontextmenu="alert('Draw our menu'); return false">
Right-click shows our context menu
</button>
Now suppose you want a document-wide context menu as well, so a right-click anywhere shows a menu — but on specific elements, a more specific menu takes over. The rule: on any right-click, the closest menu wins.
<p>Right-click here for the document context menu</p>
<button id="thumb">Right-click here for the photo context menu</button>
<script>
thumb.oncontextmenu = function(event) {
event.preventDefault();
alert("Photo context menu");
};
document.oncontextmenu = function(event) {
event.preventDefault();
alert("Document context menu");
};
</script>
Right-click the photo and you get two alerts, not one. The event fires on the photo, then bubbles up to document, so both handlers run. The photo menu shows, and then the document menu shows on top of it.
#thumbFixing it the crude way: stopPropagation
The tempting fix: “When the photo handles the right-click, stop the event from bubbling.” So you reach for event.stopPropagation():
<p>Right-click for the document menu</p>
<button id="thumb">Right-click for the photo menu (fixed with event.stopPropagation)</button>
<script>
thumb.oncontextmenu = function(event) {
event.preventDefault();
event.stopPropagation();
alert("Photo context menu");
};
document.oncontextmenu = function(event) {
event.preventDefault();
alert("Document context menu");
};
</script>
The photo menu now works. But you paid too much for it. The event no longer reaches document at all, which means any outer code loses access to this right-click — analytics counters, activity trackers, anything at all that legitimately wants to observe clicks on the page. You’ve cut off the whole neighborhood to silence one house.
Fixing it the right way: defaultPrevented
Better to leave the event bubbling and instead have the document handler check whether the click was already handled. The inner handler calls preventDefault() anyway (to suppress the browser’s own menu), and that same call flips defaultPrevented to true for the rest of the event’s journey. So the outer handler can read it:
<p>Right-click for the document menu (added a check for event.defaultPrevented)</p>
<button id="thumb">Right-click for the photo menu</button>
<script>
thumb.oncontextmenu = function(event) {
event.preventDefault();
alert("Photo context menu");
};
document.oncontextmenu = function(event) {
if (event.defaultPrevented) return;
event.preventDefault();
alert("Document context menu");
};
</script>
#thumbRight-click the photo below to see only the photo menu appear; right-click anywhere else in the box to get the document menu. Both suppress the browser’s own menu, and the outer handler bows out whenever the inner one already claimed the click:
This scales too: nest as many elements with their own context menus as you like, and it keeps working — as long as every contextmenu handler starts by checking event.defaultPrevented.
Summary
The browser has a built-in reaction for many events. A few common ones:
mousedown— begins a text selection (drag the mouse to extend it).clickon<input type="checkbox">— toggles the checkbox.submit— fires when you click an<input type="submit">or press Enter inside a form field, after which the browser submits the form.keydown— a keypress may insert a character into a field, trigger a shortcut, and so on.contextmenu— fires on right-click; the default is to open the browser’s context menu.- …and plenty more.
Any of these defaults can be canceled when you want JavaScript to own the behavior instead. Use event.preventDefault(), or return false from a handler that was assigned via on<event> — the return false form works only for that older assignment style.
The passive: true option on addEventListener promises the browser you won’t cancel the default. It pays off for touch events like touchstart and touchmove, where it lets the browser scroll immediately instead of waiting on your handlers.
After a default action is prevented, event.defaultPrevented reads true; otherwise it stays false. Reading that flag in outer handlers is a gentler alternative to stopPropagation() when you want to say “already handled” without cutting off the event’s path.