Default actions in JavaScript events
Learn when the browser performs built-in event behavior, when preventDefault can cancel it, how passive listeners affect scrolling, and how defaultPrevented coordinates handlers.
- 01Cancel a default action safelyCall
preventDefault()on events that can be canceled, and know that it does not stop the event's trip to parent elements. - 02Keep scrolling fastExplain what a passive listener is, why
wheelandtouchmovelisteners need it, and when CSS does the job better. - 03Let listeners work togetherRead
defaultPreventedin a later listener, so it can skip a click that another listener already handled.
What the browser does on its own
People click, type, scroll, and send forms on a web page. For many of these, the browser already knows what to do. This built-in behavior is called the default action.
Some default actions you already know:
- Click a link: the browser goes to the link's page.
- Send a form: the browser sends what you typed and loads a new page.
- Click a checkbox: it turns on or off.
- Press a letter key in a text box: the letter appears.
- Press Space: the page scrolls down.
- Right-click: the browser opens its own menu, called the context menu.
Default actions are why a plain HTML page works before you write any JavaScript.
Your JavaScript can listen for the same events. A listener is a function you add with addEventListener. The browser calls it when the event happens. So one click can start two kinds of work: your listeners, and the browser's default action.
First the browser runs your listeners. Then it does the default action. A listener can stop the default action by calling event.preventDefault(), but only if the event allows it.
A plane can fly itself on the usual route. This is called autopilot. Most flights use it, because it is safe and fast. One day there is a storm, so the pilot takes control for that one flight. The pilot does not switch autopilot off forever. The next flight uses it again.
- In real life: Autopilot flies the usual route
- In JavaScript: The browser goes to the link, sends the form, ticks the box, types, or scrolls
- In real life: The pilot takes control for this one flight
- In JavaScript: A listener calls
event.preventDefault()for this one event - In real life: The next flight uses autopilot again
- In JavaScript: The next click has its default action again, unless a listener cancels it again
This lesson uses ideas from three earlier lessons: Introduction to events, Bubbling & capturing, and Event delegation. The main idea you need: one event can run several listeners, one after another, and a later listener can see what an earlier one did.
preventDefault(): stop the default action
STEP THROUGHEvery listener gets an event object. It describes what happened, such as which element was clicked. One of its methods is event.preventDefault(). It tells the browser: "Don't do the default action for this event."
It only works if the event is cancelable. That means the browser allows this event's default action to be stopped. The event tells you: event.cancelable is true or false.
Two things preventDefault() does not do:
- It does not stop the event's trip. After the clicked element's listeners run, the event moves up to its parent elements, and their listeners still run. This trip is called propagation. To stop it, you call a different method,
stopPropagation(). - It does not undo something that already happened. It only stops the browser's next step.
The step-through below copies a real click with a plain object, so you can watch every line. Lines 1 to 8 make that object. Its preventDefault() sets defaultPrevented to true, but only when cancelable is true. Then come two listeners:
linkListenerstands for a listener on the link. It runs first.routerListenerstands for a listener on a parent element. It runs second. A router is code that changes what the page shows when you click a link.
Step through one click that reaches linkListener first and routerListener second. Switch line 11 to see when the router skips.
script
type: "click", cancelable: true, defaultPrevented: false, preventDefault() { if (this.cancelable) this.defaultPrevented = true; },}; function linkListener(event) { event.preventDefault(); console.log("link listener finished");} function routerListener(event) { if (event.defaultPrevented) { console.log("router skipped"); return; } console.log("router handles the click");} linkListener(event);routerListener(event);console.log(event.defaultPrevented);With line 11 calling preventDefault(), linkListener cancels the default action. But routerListener still runs, because nothing stopped the event's trip. On line 16 it reads event.defaultPrevented, sees true, and skips its own work. Switch line 11 to the other choice and run it again: now the router handles the click.
preventDefault() cancels only the browser's default action. The event still moves on, so routerListener still runs.Try real default actions
INTERACTIVEThe best way to learn default actions is to try real ones. The playground below has five real parts of a page:
- a link
- a form
- a checkbox
- a text box
- an area you can right-click
Each part has a listener, added with addEventListener. Each listener has a switch. On "prevent", the listener calls preventDefault(). On "allow", it does not, so the default action runs.
link.addEventListener("click", maybePreventLink);form.addEventListener("submit", maybePreventForm);checkbox.addEventListener("click", maybePreventCheckbox);function maybePrevent(event, switchIsOn) { if (switchIsOn && event.cancelable) event.preventDefault(); log(event.type, event.defaultPrevented);}Try real default actions
Hash (end of the address): not changed yet
- Ready. Set a switch, then try a default action.
Each part of the page here is real and has a real default action. After the listener runs, the log shows the event's cancelable and defaultPrevented values.
The code in the playground shows the idea for three of the parts. Lines 1 to 3 add a listener to the link, the form, and the checkbox. Line 5 calls preventDefault() only when the switch is on and the event is cancelable. Line 6 logs the event type and defaultPrevented.
Try these:
- Set the link to "allow" and click it. The page jumps, and the hash (the part of the address after
#) changes. - Set the checkbox to "prevent" and click it. It does not change.
- Set the input keys to "prevent" and type. No letters appear.
- Set the context menu to "allow" and right-click the area. The browser may open its own menu.
When you cancel a default action, give people something in its place:
- Stop a form from being sent? Check it and show a message, or save the data with JavaScript.
- Stop the right-click menu? Show your own menu, and give keyboard users a way to open it.
- Stop text selection? Make sure people never need to copy that text.
Passive listeners and scrolling
INTERACTIVEScrolling must feel instant. Two events can cancel scrolling:
wheel: you turn a mouse wheel or swipe on a trackpad.touchmove: your finger moves on a touch screen.
If one of these events has a listener, the listener might call preventDefault(). So the browser may have to wait for the listener to finish before it moves the page. That wait can make scrolling feel slow.
A passive listener fixes this. You add it with the option { passive: true }. It is a promise to the browser: "I will not call preventDefault()." So the browser can scroll at once, without waiting.
A passenger in a car can still talk, point, and look around. They only promised not to steer. So the driver keeps driving without looking at them.
- In real life: The passenger says, 'I will not touch the steering wheel'
- In JavaScript: The listener is added with
{ passive: true } - In real life: The driver keeps going without checking on them
- In JavaScript: The browser can start scrolling at once
- In real life: The passenger reaches for the wheel anyway; the driver ignores it
- In JavaScript: Calling
preventDefault()in a passive listener is ignored
A passive listener is the same. It can log and update things, but it cannot stop the scroll. In the playground below, line 7 sets the passive option, and line 4 tries to cancel. Turn each button on and off and scroll inside the box.
box.addEventListener("wheel", (event) => { log("cancelable", event.cancelable); if (shouldBlock) { event.preventDefault(); } log("defaultPrevented", event.defaultPrevented);}, { passive });A box to scroll
Row 1: scroll me. The browser wants this to move smoothly.
Row 2: scroll me. The browser wants this to move smoothly.
Row 3: scroll me. The browser wants this to move smoothly.
Row 4: scroll me. The browser wants this to move smoothly.
Row 5: scroll me. The browser wants this to move smoothly.
Row 6: scroll me. The browser wants this to move smoothly.
Row 7: scroll me. The browser wants this to move smoothly.
Row 8: scroll me. The browser wants this to move smoothly.
Row 9: scroll me. The browser wants this to move smoothly.
Row 10: scroll me. The browser wants this to move smoothly.
Row 11: scroll me. The browser wants this to move smoothly.
Row 12: scroll me. The browser wants this to move smoothly.
Row 13: scroll me. The browser wants this to move smoothly.
Row 14: scroll me. The browser wants this to move smoothly.
- Scroll inside the box with a mouse wheel or trackpad.
Passive is off. The listener can cancel a cancelable wheel or touchmove event, so the box can stop scrolling.
touch-action and overscroll-behavior often do the job better, because the browser knows your plan before any event fires.What to know about passive listeners:
- If a passive listener calls
preventDefault(), modern browsers ignore the call.defaultPreventedstaysfalse. - Some browsers make
touchstart,touchmove, andwheellisteners onwindow,document, orbodypassive on their own, even if you don't ask. - Use
{ passive: false }only when stopping the scroll is the whole point, like in a drawing area. - CSS often does the job better.
touch-actiontells the browser which finger gestures an element handles itself.overscroll-behaviorstops a scroll from moving on to the page behind when an inner box reaches its end.
defaultPrevented: was it canceled?
INTERACTIVEWhen preventDefault() works, the event remembers it: event.defaultPrevented becomes true. It is a true-or-false value, called a boolean.
Every later listener gets the same event object. So a later listener can read defaultPrevented and skip its own work.
defaultPrevented to true. The second one reads it and skips its own work.A team shares task cards. When someone finishes a task, they stick a "done" note on its card. Anyone who picks up the card later sees the note and moves on. They don't need to ask who did the task.
- In real life: A teammate sticks a 'done' note on a task card
- In JavaScript: An earlier listener calls
preventDefault() - In real life: Another teammate reads the note and doesn't do the task again
- In JavaScript: A later listener checks
event.defaultPrevented - In real life: The note is on that one card only
- In JavaScript: The value belongs to this one event only
The playground below has two listeners on one parent element. A button stands in for the link. The code on the left works like this:
- Lines 1 to 3: the first listener cancels the click if it was on a link.
event.targetis the element that was clicked, and.matches("a")checks that it is ana(link) element. - Lines 4 to 7: the second listener returns early if
defaultPreventedistrue. Otherwise it runs the router. - Lines 8 and 9: make a
scrollevent that is not cancelable, and callpreventDefault()on it. Nothing changes.
panel.addEventListener("click", (event) => { if (event.target.matches("a")) event.preventDefault();});panel.addEventListener("click", (event) => { if (event.defaultPrevented) return; runRouterBehavior();});const scroll = new Event("scroll", { cancelable: false });scroll.preventDefault();Click a button
- Click the link-style button. Then try the scroll event button.
The router doesn't need to know which listener ran first. It only reads event.defaultPrevented on the shared event object.
scroll event with cancelable: false. Calling preventDefault() on it changes nothing: defaultPrevented stays false.You will see this a lot with event delegation, where one listener on a parent element handles clicks for many children. The parent's listener checks defaultPrevented first. If a child already canceled the default action, the parent leaves the click alone.
Can this event be canceled?
SORT ITNot every event can stop its default action. The scroll event is the common trap. It fires after the page has already scrolled, so there is nothing left to stop. Its cancelable is false.
To stop scrolling, cancel the event that comes before the scroll: the wheel event, the touchmove event, or the key press that would scroll the page.
- Going to a link's page, from a
click - Sending a form, from a
submitevent - A checkbox turning on or off, from a
click - Text selection that starts on
mousedown - The right-click menu, from
contextmenu - The
scrollevent itself - The page closing, once it is already closing
- A letter typed into a text box, from
keydown
Sort each item: is this event the right place to call preventDefault()?
The table below has one row for each thing you might want to stop. Read across to see which event to cancel, and which event is the wrong one.
| What you want to stop | Cancel this event | Not this one |
|---|---|---|
| Going to a link's page | The link's click event. | An event that fires after the new page starts loading. It is too late. |
| A form being sent and a new page loading | The form's submit event. | A click on one submit button. Pressing Enter can send the form too. |
| A typed letter appearing | The keydown event for that key, or another event that fires before the text changes. | Reading the text box's value afterward. The letter is already there. |
| The page scrolling | The wheel, touchmove, or key event that causes the scroll. | The scroll event. It fires after the page has scrolled. |
| The right-click menu | The contextmenu event, in most browsers. | A click that comes after the menu is already open. |
Where you will use this
Cancel a default action when you replace it with your own behavior. Your behavior must work for everyone, including people who use only a keyboard. Here are the patterns you will see most.
form.addEventListener("submit", (event) => { event.preventDefault(); saveWithJavaScript(new FormData(form));});Line 2 stops the form from being sent and a new page loading. Line 3 saves the data with JavaScript instead. new FormData(form) collects every field in the form. Show people what is happening: a loading message, then a success or error message.
validationForm.addEventListener("submit", (event) => { const message = validationForm.elements.message.value.trim(); if (!message) { event.preventDefault(); validationError.textContent = "Enter a message"; return; } console.log("ready to submit", message);});- Line 2 reads the message box.
.trim()removes spaces at the start and end, so a message of only spaces counts as empty. - If the message is empty, line 4 stops the form from being sent, and line 5 shows an error message on the page.
- If there is a message, the listener does not cancel. Line 8 logs it, and the form is sent as normal.
Put this check on the form's submit event, not on the button's click. People can also send a form by pressing Enter, and submit catches every way.
modalLink.addEventListener("click", (event) => { event.preventDefault(); modal.hidden = false; modalClose.focus();}); modalClose.addEventListener("click", () => { modal.hidden = true; modalLink.focus();});A modal is a box that opens on top of the page. Here a link opens it:
- Line 2 stops the link from going to its address.
- Line 3 shows the modal by setting
hiddentofalse. - Line 4 moves keyboard focus to the modal's Close button, so keyboard users land inside it.
- Lines 8 and 9 hide the modal again and move focus back to the link.
The link still has a real address in its href. If JavaScript does not run, clicking it still goes somewhere useful.
menuArea.addEventListener("contextmenu", (event) => { event.preventDefault(); openMenu(event.clientX, event.clientY);}); menuButton.addEventListener("click", () => openMenu(0, 0));menuButton.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") openMenu(0, 0);});- Line 1 listens for
contextmenu, the event for a right-click or a long press on a touch screen. - Line 2 stops the browser's own menu, but only inside this area.
- Line 3 opens your menu where the pointer is.
clientXandclientYare the pointer's position. - Lines 6 to 8 give another way in: a visible button that opens the same menu with a click, or with Enter or Space.
In a full menu, also close it with Escape and move focus back to where it was.
input.addEventListener("keydown", (event) => { if (event.key === "x") { event.preventDefault(); }});Line 2 checks which key was pressed. Line 3 cancels only the x key, so x never appears. Every other key types as normal.
Be careful with keys. People rely on the arrow keys, Tab, Space, Enter, and browser shortcuts. Cancel only the exact key you replace, and give another way to do the same thing.
- Single-page apps
A link goes to another page on the same site. Cancel the click, change the address with the History API (the browser's tool for the address bar), and show the new content. No whole new page loads.
- Drawing and maps
In a drawing area or a map, cancel only the finger and mouse moves your app handles itself. Use CSS
touch-actionwhen it can do the job. - Widgets inside a parent
A child widget calls
preventDefault(). The listener on the parent checksdefaultPreventedbefore it does anything.
Common mistakes
COMMON TRAPS- "preventDefault() stops bubbling." No. It only cancels the default action. The event still moves up to the parent elements. Stopping that is a separate choice.
- "stopPropagation() stops the default action." No. It only stops the event from reaching parent elements. A link still goes to its page.
- "return false always cancels." Not in a listener added with
addEventListener. There, the browser ignores what you return. CallpreventDefault()instead. - "Passive is just a faster way to write a listener." No. Passive means the listener cannot cancel the default action.
- "Every event has a default action you can cancel." No. Check
event.cancelable, and cancel the right earlier event. - "Stopping a default action is harmless." Many default actions help people who use a keyboard or a screen reader. If you remove one, replace it.
button.addEventListener("click", (event) => { return false; // does not cancel an addEventListener default}); button.onclick = function (event) { return false; // old property handlers cancel the default};- Line 2 returns
falsefrom a listener added withaddEventListener. The browser ignores it, so the default action still happens. - Line 6 returns
falsefrom an old-styleonclickhandler, a function stored in the element'sonclickproperty. There,falsedoes cancel the default action.
This difference is old and easy to mix up. Always call event.preventDefault(), so anyone reading your code knows what you mean.
The table below compares four tools. Read across each row to see what the tool changes and what it leaves alone.
| Tool | What it changes | What it leaves alone |
|---|---|---|
preventDefault() | It cancels the default action, if the event is cancelable. | The event still moves up to the parent elements. |
stopPropagation() | The parent elements' listeners don't get the event. | The default action still happens. |
{ passive: true } | The listener can no longer cancel the default action. | The listener can still read the event and run code. |
defaultPrevented | Nothing. It only tells you whether the default action was canceled. | It cannot make an event cancelable. |
Practice
5 EXERCISESWrite the three lines the console prints, in order. Separate them with commas.
const event = { defaultPrevented: false, cancelable: true };
event.preventDefault = function () {
if (this.cancelable) this.defaultPrevented = true;
};
function linkListener(event) {
event.preventDefault();
console.log("link listener finished");
}
function routerListener(event) {
if (event.defaultPrevented) {
console.log("router skipped");
return;
}
console.log("router handles the click");
}
linkListener(event);
routerListener(event);
console.log(event.defaultPrevented);linkListener calls preventDefault(), so defaultPrevented becomes true. Then it logs link listener finished. routerListener sees true, logs router skipped, and returns. The last line logs true.
Predict the one line the console prints.
const scroll = new Event("scroll", { cancelable: false });
scroll.preventDefault();
console.log(scroll.defaultPrevented);The scroll event was made with cancelable: false. Calling preventDefault() does not throw an error. It does nothing, so defaultPrevented stays false.
Which method goes inside the if block to stop the x key?
input.addEventListener("keydown", (event) => {
if (event.key === "x") {
event.preventDefault();
}
});input.addEventListener("keydown", (event) => {
if (event.key === "x") {
event.preventDefault();
}
});The default action of a letter key is to add the letter to the text box. Calling event.preventDefault() in the keydown listener stops that, but only for the key you check.
The developer wanted the link to stay on this page. But it still goes to its page. Explain the bug, then compare your fix with the solution.
link.addEventListener("click", (event) => {
saveDraft();
return false;
});link.addEventListener("click", (event) => {
event.preventDefault();
saveDraft();
});The browser ignores return false in a listener added with addEventListener. Call event.preventDefault() to stop the link, then run saveDraft() in its place.
Plan the events for your own right-click menu in a drawing app. Say which event you cancel. Then give one other way to open the menu, for example for keyboard users.
menuArea.addEventListener("contextmenu", (event) => {
event.preventDefault();
openCustomMenu({ x: event.clientX, y: event.clientY });
});
menuButton.addEventListener("click", () => openCustomMenu());Cancel contextmenu only in the area where your menu works. Also add a normal button, so people without a mouse can still open the menu.
Check your understanding
7 QUESTIONSQuestion 1 of 7When does the browser usually do the default action for an event?
Choose an answer to see the explanation.
Question 2 of 7What does this code print? The scroll event is not cancelable.
Read the code, then predictconst event = new Event("scroll", { cancelable: false }); event.preventDefault(); console.log(event.defaultPrevented);Choose an answer to see the explanation.
Question 3 of 7Which sentence about
preventDefault()is true?Choose an answer to see the explanation.
Question 4 of 7What does this code print? The click event is cancelable.
Read the code, then predictconst event = new Event("click", { cancelable: true }); event.preventDefault(); console.log(event.defaultPrevented);Choose an answer to see the explanation.
Question 5 of 7A passive
wheellistener callsevent.preventDefault(). What happens?Choose an answer to see the explanation.
Question 6 of 7What does
return falsedo inside a listener added withaddEventListener?Choose an answer to see the explanation.
Question 7 of 7Why would a router listener on a parent element check
event.defaultPrevented?Choose an answer to see the explanation.
Key takeaways
- A default action is what the browser does on its own after an event.
preventDefault()works only on cancelable events. A passive listener cannot cancel at all.- The browser does the default action after every listener has run. So a later listener can still read
defaultPrevented. - Canceling the default action and stopping the event's trip to parent elements are two separate choices.
- Many default actions help people. Don't remove keyboard, scrolling, selection, form, or menu behavior without giving something in its place.
A default action is what the browser does on its own after an event. event.preventDefault() cancels it, but only when the event is cancelable and the listener is not passive.
Up next: Custom events.