preventDefault
and stopPropagation
: when to use which and what exactly each method does.
Event.stopPropagation() and Event.preventDefault()
JavaScript event handling is often straightforward. This is especially true when dealing with a
simple (relatively flat) HTML structure. Things get a bit more involved though when events are
traveling (or propagating) through a hierarchy of elements. This is typically when developers reach
for stopPropagation()
and/or preventDefault()
to solve the problems they're experiencing. If
you've ever thought to yourself "I'll just try preventDefault()
and if that doesn't work I'll try
stopPropagation()
and if that doesn't work, I'll try both," then this article is for you! I will
explain exactly what each method does, when to use which one, and provide you with a variety of
working examples for you to explore. My goal is to end your confusion once and for all.
Before we dive too deeply though, it's important to briefly touch on the two kinds of event handling possible in JavaScript (in all modern browsers that is—Internet Explorer prior to version 9 did not support event capturing at all).
Eventing styles (capturing and bubbling)
All modern browsers support event capturing, but it is very rarely used by developers.
Interestingly, it was the only form of eventing that Netscape originally supported. Netscape's
biggest rival, Microsoft Internet Explorer, did not support event capturing at all, but rather, only
supported another style of eventing called event bubbling. When the W3C was formed, they found merit
in both styles of eventing and declared that browsers should support both, via a third parameter to
the addEventListener
method. Originally, that parameter was just a boolean, but all modern
browsers support an options
object as the third parameter, which you can use to specify (among
other things) if you want to use event capturing or not:
someElement.addEventListener('click', myClickHandler, { capture: true | false });
Note that the options
object is optional, as is its capture
property. If either is omitted, the
default value for capture
is false
, meaning event bubbling will be used.
Event capturing
What does it mean if your event handler is "listening in the capturing phase?" To understand this, we need to know how events originate and how they travel. The following is true of all events, even if you, as the developer, don't leverage it, care about it, or think about it.
All events begin at the window and first go through the capturing phase. This means that when an event is dispatched, it starts the window and travels "downwards" towards its target element first. This happens even if you are only listening in the bubbling phase. Consider the following example markup and JavaScript:
<html>
<body>
<div id="A">
<div id="B">
<div id="C"></div>
</div>
</div>
</body>
</html>
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('#C was clicked');
},
true,
);
When a user clicks on element #C
, an event, originating at the window
, is dispatched. This event
will then propagate through its descendants as follows:
window
=> document
=> <html>
=> <body>
=> and so on, until it reaches the target.
It does not matter if nothing is listening for a click event at the window
or document
or
<html>
element or <body>
element (or any other element on the way to its target). An event still
originates at the window
and begins its journey as just described.
In our example, the click event will then propagate (this is an important word as it will tie
directly into how the stopPropagation()
method works and will be explained later in this document)
from the window
to its target element (in this case, #C
) by way of every element between the
window
and #C
.
This means that the click event will begin at window
and the browser will ask the following
questions:
"Is anything listening for a click event on the window
in the capturing phase?" If so, the
appropriate event handlers will fire. In our example, nothing is, so no handlers will fire.
Next, the event will propagate to the document
and the browser will ask: "Is anything listening
for a click event on the document
in the capturing phase?" If so, the appropriate event handlers
will fire.
Next, the event will propagate to the <html>
element and the browser will ask: "Is anything
listening for a click on the <html>
element in the capturing phase?" If so, the appropriate event
handlers will fire.
Next, the event will propagate to the <body>
element and the browser will ask: "Is anything
listening for a click event on the <body>
element in the capturing phase?" If so, the appropriate
event handlers will fire.
Next, the event will propagate to the #A
element. Again, the browser will ask: "Is anything
listening for a click event on #A
in the capturing phase and if so, the appropriate event handlers
will fire.
Next, the event will propagate to the #B
element (and the same question will be asked).
Finally, the event will reach its target and the browser will ask: "Is anything listening for a
click event on the #C
element in the capturing phase?" The answer this time is "yes!" This brief
period of time when the event is at the target, is known as the "target phase." At this point, the
event handler will fire, the browser will console.log "#C was clicked" and then we're done, right?
Wrong! We're not done at all. The process continues, but now it changes to the bubbling phase.
Event bubbling
The browser will ask:
"Is anything listening for a click event on #C
in the bubbling phase?" Pay close attention here.
It is completely possible to listen for clicks (or any event type) in both the capturing and
bubbling phases. And if you had wired up event handlers in both phases (e.g. by calling
.addEventListener()
twice, once with capture = true
and once with capture = false
), then yes,
both event handlers would absolutely fire for the same element. But it's also important to note that
they fire in different phases (one in the capturing phase and one in the bubbling phase).
Next, the event will propagate (more commonly stated as "bubble" because it seems as though the
event is traveling "up" the DOM tree) to its parent element, #B
, and the browser will ask: "Is
anything listening for click events on #B
in the bubbling phase?" In our example, nothing is, so
no handlers will fire.
Next, the event will bubble to #A
and the browser will ask: "Is anything listening for click
events on #A
in the bubbling phase?"
Next, the event will bubble to <body>
: "Is anything listening for click events on the <body>
element in the bubbling phase?"
Next, the <html>
element: "Is anything listening for click events on the <html>
element in the
bubbling phase?
Next, the document
: "Is anything listening for click events on the document
in the bubbling
phase?"
Finally, the window
: "Is anything listening for click events on the window in the bubbling phase?"
Phew! That was a long journey, and our event is probably very tired by now, but believe it or not, that is the journey every event goes through! Most of the time, this is never noticed because developers are typically only interested in one event phase or the other (and it is usually the bubbling phase).
It's worth spending some time playing around with event capturing and event bubbling and logging some notes to the console as handlers fire. It's very insightful to see the path that an event takes. Here is an example that listens to every element in both phases.
<html>
<body>
<div id="A">
<div id="B">
<div id="C"></div>
</div>
</div>
</body>
</html>
document.addEventListener(
'click',
function (e) {
console.log('click on document in capturing phase');
},
true,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
'click',
function (e) {
console.log('click on <html> in capturing phase');
},
true,
);
document.body.addEventListener(
'click',
function (e) {
console.log('click on <body> in capturing phase');
},
true,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('click on #A in capturing phase');
},
true,
);
document.getElementById('B').addEventListener(
'click',
function (e) {
console.log('click on #B in capturing phase');
},
true,
);
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('click on #C in capturing phase');
},
true,
);
document.addEventListener(
'click',
function (e) {
console.log('click on document in bubbling phase');
},
false,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
'click',
function (e) {
console.log('click on <html> in bubbling phase');
},
false,
);
document.body.addEventListener(
'click',
function (e) {
console.log('click on <body> in bubbling phase');
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('click on #A in bubbling phase');
},
false,
);
document.getElementById('B').addEventListener(
'click',
function (e) {
console.log('click on #B in bubbling phase');
},
false,
);
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('click on #C in bubbling phase');
},
false,
);
The console output will depend on which element you click. If you were to click on the "deepest"
element in the DOM tree (the #C
element), you will see every single one of these event handlers
fire. With a bit of CSS styling to make it more obvious which element is which, here is the console
output #C
element (with a screenshot as well):
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
"click on <body> in bubbling phase"
"click on <html> in bubbling phase"
"click on document in bubbling phase"
You can interactively play with this in the live demo below. Click on the #C
element and observe the console output.
event.stopPropagation()
With an understanding of where events originate and how they travel (i.e. propagate) through the DOM
in both the capturing phase and the bubbling phase, we can now turn our attention to
event.stopPropagation()
.
The stopPropagation()
method can be called on (most) native DOM events. I say "most" because there
are a few on which calling this method won't do anything (because the event doesn't propagate to
begin with). Events like focus
, blur
, load
, scroll
, and a few others fall into this
category. You can call stopPropagation()
but nothing interesting will happen, since these events
don't propagate.
But what does stopPropagation
do?
It does, pretty much, just what it says. When you call it, the event will, from that point, cease
propagating to any elements it would otherwise travel to. This is true of both directions
(capturing and bubbling). So if you call stopPropagation()
anywhere in the capturing phase, the
event will never make it to the target phase or bubbling phase. If you call it in the bubbling
phase, it will have already gone through the capturing phase, but it will cease "bubbling up" from
the point at which you called it.
Returning to our same example markup, what do you think would happen, if we called
stopPropagation()
in the capturing phase at the #B
element?
It would result in the following output:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
You can interactively play with this in the live demo below. Click on the #C
element in the live demo and observe the console output.
How about stopping propagation at #A
in the bubbling phase? That would result in the following
output:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
You can interactively play with this in the live demo below. Click on the #C
element in the live demo and observe the console output.
One more, just for fun. What happens if we call stopPropagation()
in the target phase for #C
?
Recall that the "target phase" is the name given to the period of time when the event is at its
target. It would result in the following output:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
Note that the event handler for #C
in which we log "click on #C in the capturing phase" still
executes, but the one in which we log "click on #C in the bubbling phase" does not. This should make
perfect sense. We called stopPropagation()
from the former, so that is the point at which the
event's propagation will cease.
You can interactively play with this in the live demo below. Click on the #C
element in the live demo and observe the console output.
In any of these live demos, I encourage you to play around. Try clicking on the #A
element only or
the body
element only. Try to predict what will happen and then observe if you are correct. At
this point, you should be able to predict pretty accurately.
event.stopImmediatePropagation()
What is this strange, and not oft-used method? It's similar to stopPropagation
, but rather than
stopping an event from traveling to descendents (capturing) or ancestors (bubbling), this method
only applies when you have more than one event handler wired up to a single element. Since
addEventListener()
supports a multicast style of eventing, it's completely possible to wire up an
event handler to a single element more than once. When this happens, (in most browsers), event
handlers are executed in the order they were wired up. Calling stopImmediatePropagation()
prevents
any subsequent handlers from firing. Consider the following example:
<html>
<body>
<div id="A">I am the #A element</div>
</body>
</html>
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I shall run first!');
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I shall run second!');
e.stopImmediatePropagation();
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I would have run third, if not for stopImmediatePropagation');
},
false,
);
The above example will result in the following console output:
"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"
Note that the third event handler never runs due to the fact that the second event handler calls
e.stopImmediatePropagation()
. If we had instead called e.stopPropagation()
, the third handler
would still run.
event.preventDefault()
If stopPropagation()
prevents an event from traveling "downwards" (capturing) or "upwards"
(bubbling), what then, does preventDefault()
do? It sounds like it does something similar. Does
it?
Not really. While the two are often confused, they actually don't have much to do with each other.
When you see preventDefault()
, in your head, add the word "action." Think "prevent the default
action."
And what is the default action you may ask? Unfortunately, the answer to that isn't quite as clear because it's highly dependent on the element + event combination in question. And to make matters even more confusing, sometimes there is no default action at all!
Let's begin with a very simple example to understand. What do you expect to happen when you click a
link on a web page? Obviously, you expect the browser to navigate to the URL specified by that link.
In this case, the element is an anchor tag and the event is a click event. That combination (<a>
+
click
) has a "default action" of navigating to the link's href. What if you wanted to prevent
the browser from performing that default action? That is, suppose you want to prevent the browser
from navigating to the URL specified by the <a>
element's href
attribute? This is what
preventDefault()
will do for you. Consider this example:
<a id="avett" href="https://www.theavettbrothers.com/welcome">The Avett Brothers</a>
document.getElementById('avett').addEventListener(
'click',
function (e) {
e.preventDefault();
console.log('Maybe we should just play some of their music right here instead?');
},
false,
);
You can interactively play with this in the live demo below. Click the link The Avett Brothers and observe the console output (and the fact that you are not redirected to the Avett Brothers website).
Normally, clicking the link labelled The Avett Brothers would result in browsing to
www.theavettbrothers.com
. In this case though, we've wired up a click event handler to the <a>
element and specified that the default action should be prevented. Thus, when a user clicks this
link, they won't be navigated anywhere, and instead the console will simply log "Maybe we should
just play some of their music right here instead?"
What other element/event combinations allow you to prevent the default action? I cannot possibly list them all, and sometimes you have to just experiment to see. But briefly, here are a few:
<form>
element + "submit" event:preventDefault()
for this combination will prevent a form from submitting. This is useful if you want to perform validation and should something fail, you can conditionally call preventDefault to stop the form from submitting.<a>
element + "click" event:preventDefault()
for this combination prevents the browser from navigating to the URL specified in the<a>
element's href attribute.document
+ "mousewheel" event:preventDefault()
for this combination prevents page scrolling with the mousewheel (scrolling with keyboard would still work though).
↜ This requires callingaddEventListener()
with{ passive: false }
.document
+ "keydown" event:preventDefault()
for this combination is lethal. It renders the page largely useless, preventing keyboard scrolling, tabbing, and keyboard highlighting.document
+ "mousedown" event:preventDefault()
for this combination will prevent text highlighting with the mouse and any other "default" action that one would invoke with a mouse down.<input>
element + "keypress" event:preventDefault()
for this combination will prevent characters typed by the user from reaching the input element (but don't do this; there is rarely, if ever, a valid reason for it).document
+ "contextmenu" event:preventDefault()
for this combination prevents the native browser context menu from appearing when a user right-clicks or long-presses (or any other way in which a context menu might appear).
This is not an exhaustive list by any means, but hopefully it gives you a good idea of how
preventDefault()
can be used.
A fun practical joke?
What happens if you stopPropagation()
and preventDefault()
in the capturing phase, starting at
the document? Hilarity ensues! The following code snippet will render any web page just about
completely useless:
function preventEverything(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
document.addEventListener('click', preventEverything, true);
document.addEventListener('keydown', preventEverything, true);
document.addEventListener('mousedown', preventEverything, true);
document.addEventListener('contextmenu', preventEverything, true);
document.addEventListener('mousewheel', preventEverything, { capture: true, passive: false });
I don't really know why you'd ever want to do this (except maybe to play a joke on someone), but it is useful to think about what's happening here, and realize why it creates the situation it does.
All events originate at window
, so in this snippet, we're stopping, dead in their tracks, all
click
, keydown
, mousedown
, contextmenu
, and mousewheel
events from ever getting to any
elements that might be listening for them. We also call stopImmediatePropagation
so that any
handlers wired up to the document after this one, will be thwarted as well.
Note that stopPropagation()
and stopImmediatePropagation()
aren't (at least not mostly) what
render the page useless. They simply prevent events from getting where they would otherwise go.
But we also call preventDefault()
, which you'll recall prevents the default action. So any
default actions (like mousewheel scroll, keyboard scroll or highlight or tabbing, link clicking,
context menu display, etc.) are all prevented, thus leaving the page in a fairly useless state.
Live demos
To explore all the examples from this article again in one place, check out the embedded demo below.
Acknowledgements
Hero image by Tom Wilson on Unsplash.