A Few DOM Reminders
Millions of people work with HTML every day and billions consume it. But how many of us remember or ever knew the fundamentals? With this in mind, I thought it good to take some time to remind myself of a few details as pertain to HTML and the DOM.
What is HTML?
Created in 1990 at CERN, HTML stands for “HyperText Markup Language” and is used to describe linked documents. While there are many interesting details to HTML, I personally find the combination of the following most notable:
Declarative
Rather than write code to tell the browser how to render a document, you instead describe what your document is composed of. Then the browser goes off and figures out all the nitty gritty details of how to make that happen and presents it to the user in the way most appropriate to them.
NOTE: Most modern UI systems take a similarly declarative approach today, but that wasn’t always the case. Some systems later patterned themselves explicitly after HTML as well. For example, Microsoft’s Windows Presentation Foundation (WPF) team was composed of a number of individuals who formerly worked on Internet Explorer and HTML. In fact, during the early explorations of WPF, Microsoft considered building their entire next-generation UI stack on HTML (circa 2002). Instead, they decided to create Xaml and WPF. I often wonder what would have happened if they had stuck with HTML and contributed everything back to standards.
Semantic
As a declarative language, HTML further focuses on semantic documents. In otherwards, you don’t typically declare meaningless squares and circles, but rather meaningful sections, headers, paragraphs, etc. This turns out to make HTML a deeply accessible and portable format. Screen readers can more easily understand what a paragraph is and find the best way to communicate that to a non-sighted user compared to needing to interpret a set of raw shapes and text blocks. Various devices such as phones, watches, TVs, and VR headsets can also interpret the document in the way most appropriate to their form factor.
Furthermore, the clear separation of semantics and presentation that HTML provides has not only proven to be of technical benefit but has also expanded the potential for expressiveness, diversity, and creativity that takes place on the platform.
ASIDE: While HTML is semantic, it nevertheless always has some concrete presentation via the user agent. The armchair philosopher inside of me can’t help but draw a connection to the Aristotelian Thomistic idea of Hylomorphism which posits that all beings are a composition of form and matter.
Hyperlinks
HTML contains the core primitive that gave birth to the web itself: the hyperlink. The hyperlink creates connections between documents, sections of documents, and apps, even across computers. While the idea of a “link” seems like such an obvious, small, and simple thing, the way hyperlinks enabled breaking free of a computer’s memory space to connect globally, changed the world.
Streaming
HTML is a text format, but the browser doesn’t require downloading a full text document before it can do anything. The streaming nature of HTML enables the browser to begin rendering content before the server response has completed. This detail, which is often forgotten, is extremely important to getting your content in front of people as fast as possible and is a key differentiator from native app platforms.
There is much more to say about what makes HTML interesting, but this particular combination is what stands out to me these days.
Structuring an HTML Document
Every HTML document you write will always have the same basic top-level structure. First, the document begins with a DOCTYPE
to ensure that the browser uses a rendering mode that is consistent with the HTML specification:
<!DOCTYPE html>
Following the DOCTYPE
we see our first HTML element, aptly named html
.
<!DOCTYPE html>
<html lang="en">
</html>
Here are a few general details about elements in HTML:
- Elements have a name and opening and closing tags. The opening tag takes the form
<tagname>
and the closing tag takes the form</tagname>
. Tag name casing is normalized by the browser, so there’s no difference between “MyTag” and “mytag”. However, if you see a hyphen (-
) in the name, that is significant. That usually indicates it’s a custom element (though it could also be what’s called anHTMLUnkownElement
). - Elements can have attributes, such as
lang
shown above. A Content Attribute has a name and a value in the formname="value"
. A Boolean Attribute has just a name, such asdisabled
. Attribute name casing is normalized, but the casing of their values is preserved. - Elements can have content between their opening and closing tags, including other elements, text, comments, etc. The casing of text and comment content is preserved.
The html
element is a special element in that it is the root of the document and contains two specific child elements along with their content:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Greetings</title>
<link rel="icon" type="image/x-icon" href="/images/favicon.ico">
</head>
<body>
<p>Hello World!</p>
<!--This is a comment.-->
</body>
</html>
The head
element comes first and primarily contains metadata about the document. The most common elements found in the head are:
meta
— These elements serve as a general form of metadata and can provide a variety of details about the document. Above, themeta
element declares the document’s character encoding with thecharset
attribute.title
— A document should always have atitle
element that specifies the document’s title. This is typically displayed in the browser tab.link
— These elements identify resources that are related to the current document. Therel
attribute describes the nature of the relationship and thehref
indicates where to find the resource. The optionaltype
attribute describes the media type of the referenced resource. In the example above we’re providing an image that relates to the document as its icon, typically referred to as a favicon. This is often rendered in the browser tab next to thetitle
. Besides the favicon, the most common type of link is typically a stylesheet. Read more about links here.
The second child of the html
element is the body
element. The body
contains the semantic content of the document itself and typically contains the majority of the markup. In the above example, there’s a p
element, which represents a paragraph. The paragraph contains the text “Hello World!” There’s also a comment, which serves the same purpose as comments in other languages.
IMPORTANT: Did you notice that the
meta
andlink
elements above have no closing tag? Earlier I stated that elements have an opening and closing tag. Actually, there are a few exceptions to this rule. The HTML specification defines a fixed set of elements, known as “void elements”, which have no children and should not have an end tag. This is different from XML’s idea of self-closing tags. For more background on this subject, see Jake Archibald’s “The case against self-closing tags in HTML”.
The Document Object Model (DOM)
The browser converts a text-based HTML document into an in-memory object model. This conversion is handled by its HTML parser, which can stream the content from the response directly, choosing at what points to pause parsing and yield to the render task. The object model it creates along the way is referred to as the Document Object Model or DOM for short.
IMPORTANT: Yes, you read that right. The parser can yield to the renderer at various points while processing the request stream. As a result, the browser could begin rendering an element before it has parsed any or all of its child nodes. This is part of HTML’s streaming superpower, but if you don’t know about this it can be surprising. Watch out for front-end libraries and frameworks with SSR approaches that prevent the browser from using its streaming capabilities. This will affect your user’s experience.
Above, we’ve primarily discussed how an HTML document is composed of elements, but there’s more to the DOM than that. For example, the text and comment shown above are not elements. The parser will turn those into instances of Text
and Comment
respectively, neither of which is an Element
. However, both of them, along with Element
, inherit from a lower-level type: Node
.
It turns out that the DOM is a generic tree data structure composed of Node
instances. There are many different types of nodes which compose together in various ways. Let’s go over the broad types of nodes.
EventTarget
The core Node
type that the DOM is built on actually inherits from an even lower-level type: EventTarget
. This type implements the basic APIs for receiving and dispatching events and is the foundation not only of the DOM but also a number of other familiar types such as XMLHttpRequest
. Interestingly, you can directly instantiate an EventTarget
and use it independently of the DOM as an event aggregator both in the browser and in Node.js.
Node
The common base type of the DOM’s general tree structure is appropriately named Node
. The Node
type provides many useful APIs. Here are a few that I have found myself using the most over the years. I’ve organized them into three broad categories:
Relationships
childNodes
— A list of all the child nodes of the current node.firstChild
,lastChild
,nextSibling
,previousSibling
,parentNode
,parentElement
— These properties point to other nodes that stand in the named relationship to the current node. They returnnull
if there is no node in that relationship.isConnected
— Aboolean
property that indicates whether the node is connected to the document. If the node is connected to a shadow root but that shadow root is not transitively connected to the document, this property returnsfalse
.contains(node)
— Indicates whether the provided node is a descendant of the current node.getRootNode()
— If the node is within a shadow tree, this returns the shadow root, otherwise it returns the document.
Metadata
nodeType
— A number that indicates the type of the node. For example, an element will have a value of1
while a text node will have a value of3
.nodeName
— Provides the tag name of an element or a special text representation if the node is not an element. For example, a text node will have the value#text
.
Manipulation
appendChild(node)
— Adds the new node to the end of the child nodes.insertBefore(node, childLocationNode)
— Inserts a node before the specified location node.removeChild(node)
— Removes the specified child node.replaceChild(newNode, currentChildNode)
— Replaces the current node with the new node.cloneNode(deep)
— Makes a clone of the current node. Passtrue
to deeply clone the entire subtree.
Check out the rest of the Node
APIs and learn more on MDN.
ASIDE: What’s up with the
parentElement
property? Why does a base class have a property that references a concept from a subclass? Shouldn’t this only exist onElement
instances but notNode
instances? To be honest, I’m not sure what the history is around adding this API here. In OOP, this would be considered a bad practice. My guess is that this was added purely for convenience. The DOM is a funny creature. 😉
Element
Several important types extend from Node
, not the least of which is Element
. Element
is the base type for a few other very important types: HTMLElement
, SVGElement
, and MathMLElement
. Similar to Node
, Element
provides a number of very useful APIs. Here are a few of the ones I’ve found most handy over the years:
Relationships
attributes
— A list of all theAttr
nodes on the element.children
— A list of all the child elements. Do not confuse this withNode#childNodes
, which returns all the child nodes, not just elements.closest(selector)
— Finds the first or “closest” CSS selector match in the element ancestor hierarchy relative to the current element.firstElementChild
,lastElementChild
,nextElementSibling
,previousElementSibling
— These properties point to other elements that stand in the named relationship to the current element. They returnnull
if there is no element in that relationship. This doesn’t mean that there isn’t a non-elementNode
in that relationship though. e.g. The previous node might beText
.
Metadata
assignedSlot
— If the DOM element is being projected into a Shadow DOM, this property will provide theslot
instance where the element is being projected.classList
andclassName
— Used to both read and write classes associated with the element.classList
is an instance ofDOMTokenList
, which provides a richer API for adding, removing, toggling, and iterating classes.id
— The id of the element, if assigned.matches(selector)
— Indicates whether the current element matches the provided CSS selector.part
— The CSS parts that have been assigned to the element. Similar toclassList
, this is an instance ofDOMTokenList
, with a rich API for manipulating parts.querySelector(selector)
andquerySelectorAll(selector)
— Finds Light DOM descendant elements that match the provided selectors.slot
— The name of the Shadow DOM slot that the element is to be assigned to.tagName
— The element’s tag name in HTML. Bear in mind that this casing is normalized to all caps.localName
— Similar totagName
but lowercase and without any xml prefix (if relevant).
Manipulation
aria*
— Many ARIA related properties can be used onElement
instances to support accessibility scenarios.after(...nodes)
— Inserts strings or nodes just after the current element.append(...nodes)
— Adds strings or nodes to the end of the child nodes of the current element.before(...nodes)
— Adds strings or nodes just before the current element.attachShadow(options)
— Attaches a Shadow DOM to the element.innerHTML
— The HTML representing the Light DOM child nodes of the element, which can be read or written to. When writing to this, exercise extreme caution, as this is an attack vector. Always use this in combination with TrustedTypes or instead consider thesetHTML(...)
API where it’s supported.insertAdjacentElement(position, element)
,insertAdjacentHTML(position, htmlText)
, andinsertAdjacentText(position, charData)
— Inserts the respective node types or HTML strings according to the specified position relative to the current element. Theposition
can be “beforebegin”, “afterbegin”, “beforeend”, or “afterend”.prepend(...nodes)
— Inserts nodes or strings at the beginning of the current element’s child nodes.shadowRoot
— If an open shadow root was attached to the element, it can be manipulated through this property. If it’s closed or one has not been attached, this will benull
.getAttribute(name)
,setAttribute(name, value)
,hasAttribute(name)
,removeAttribute(name)
, andtoggleAttribute(name, force)
— Used to read and manipulate attributes of the element.remove()
— Removes the current element from its parent’s child nodes.
NOTE: What is the difference between
append(...nodes)
onElement
andappendChild(node)
onNode
? An obvious difference is thatappend()
accepts multiple nodes whileappendChild()
only accepts one. However, an additional difference is thatappend()
also accepts strings, which it will automatically turn intoText
nodes.
There is so much functionality built into the Element
class that I have barely scratched the surface with this list. Be sure to explore more on MDN.
HTMLElement
As mentioned, HTMLElement
builds on top of Element
, adding even more APIs particular to the HTML-specific branch of the DOM, as opposed to SVGElement
or MathMLElement
. This is the part of HTML that most developers are familiar with. Again, a few notable APIs include:
Metadata
dataset
— Provides read/write access to the element’sdata-
attributes.dir
— Indicates the directionality of the element.hidden
— Indicates whether thehidden
attribute is present.lang
— Indicates the language of the element and its contents.
Manipulation
attachInternals()
— Used from within a Web Component to initialize theElementInternals
API, providing deeper web platform integration with forms, validation, states, and ARIA.blur()
— Removes keyboard focus from the element.contentEditable
— Gets or sets whether the element’s content is editable by the user. The value is the stringtrue
orfalse
.draggable
— Makes the element draggable.focus()
— Directs keyboard focus to the current element.style
— Enables manipulating the element’s styles via an instance ofCSSStyleDeclaration
.tabIndex
— Enables controlling where the element exists in the tab order.title
— The text to be displayed on hover.
As always, there are many more APIs to explore, so be sure to check out MDN. This reference of all built-in element tags and this blog post are also quite useful. Many of the specific subclasses of HTMLElement
will also have their own additional APIs according to their purpose.
NOTE: Not every HTML tag has a specific type in the DOM. For example, a
<p>
element will be an instance ofHTMLParagraphElement
, but a<section>
element will be an instance ofHTMLElement
. This pattern typically applies to semantic-only tags such as<section>
,<article>
,<header>
,<footer>
, etc. If you are searching on MDN for the related DOM type for an element, start by typing “HTML” and then the first letter of the tag. For example, “HTMLP”. If there is a special DOM type, you will see it in the search results. If you do not see something with a name in the form “HTML***Element” then it is probably a purely semantic element represented byHTMLElement
itself.
SVGElement and MathMLElement
Even though SVG and MathML are XML, they are parsed by the HTML parser when they appear in an HTML document using foreign content mode. At runtime, the instances created as a result of parsing inherit from the same Element
base class noted above. SVGElement
and MathMLElement
each add a few of their own unique APIs as well. Neither is anywhere as extensive as HTMLElement
or the Element
base type.
DocumentFragment
A DocumentFragment
is a very useful type because it represents a list of nodes without a parent. It is most commonly found as the content
property of an HTMLTemplateElement
. However, you can create a fragment directly by using document.createDocumentFragment()
or new DocumentFragment()
. While a fragment does not inherit from Element
it does have some APIs in common. Typically, fragments are created with HTML that will be needed repeatedly. The cloneNode(true)
API is used to clone the fragment and then one of the Element
manipulation APIs is used to append the cloned fragment to the DOM.
ASIDE: Templating engines often make heavy use of this primitive in their rendering/hydration engines.
Attr
It might surprise you, but Attr
actually inherits from Node
. Typically accessed from Element#attributes
the Attr
type provides several useful APIs for working with attributes:
name
— The readonly name of the attribute.value
— The read/write value of the attribute.ownerElement
— TheElement
instance on which the attribute is defined.
There are actually three different types of attributes as well:
- Content Attributes — The most common attribute with a name and a value. e.g.
src="...”
href="..."
- Boolean Attributes — These are either present or absent, representing
true
orfalse
. e.g.hidden
- IDL Attributes — These aren’t really “attributes” at all and won’t be in the
attributes
collection onElement
. Rather, they are JavaScript properties on DOM objects. Sometimes their names map 1:1 to a Content or Boolean attribute. Sometimes they don’t. An example of where they don’t is the<label>
for
Content Attribute that corresponds to thehtmlFor
IDL Attribute onHTMLLabelElement
. The other important detail to know about IDL Attributes is that sometimes they reflect their property changes to the Content/Boolean attribute and sometimes they don’t. An example where changes are not reflected is theinput
'svalue
property (IDL Attribute), which gets its initial value from the input’s value Content Attribute but does not write changes back to the Content Attribute. To get the current value, you must access the property.
NOTE: Why doesn’t an
input
'svalue
property write back to itsvalue
Content Attribute? There may be multiple reasons for this, but one reason is certainly to support form resets. When a form reset occurs, the browser needs a piece of state to tell it what the input should be reset to. It uses thevalue
Content Attribute for this. If thevalue
property were to update the attribute, it would erase the data needed to enable form reset.IMPORTANT: When cloning nodes, IDL Attribute values aren’t copied. Only Content and Boolean Attributes are. These are the only values the runtime knows how to reliably and safely clone. Be aware of this, particularly when creating custom elements with state stored in IDL Attributes (properties).
NOTE: Most folks not working on specs will refer to IDL Attributes as properties. It’s important to know the terminology in case you come across it though. Otherwise, it can be quite confusing.
CharacterData
Just as Element
was the base for all the element types in the DOM, CharacterData
is the base for all the character or text-oriented node types. These include Text
, Comment
, CDataSection
, and ProcessingInstruction
. Even though there is no inheritance relationship with Element
, CharacterData
has a number of the same APIs for traversing and modifying the DOM. It’s worth a quick review on MDN to see what’s available. A few of the APIs that are unique to CharacterData
include:
data
— Gets or sets the actual text.length
— The length of the text.appendData(data)
,deleteData(index, length)
,insertData(index, data)
,replaceData(index, length, data)
— Performs the named operation to manipulate the current text data of the node.replaceWith(nodes)
— Replaces the entireCharacterData
node with a different set of nodes.substringData(index, length)
— Returns a substring of the character data.
Text
Text
inherits from CharacterData
and is typically created either by calling new Text(data)
or document.createTextNode(data)
. There are a couple of interesting additional APIs for Text
:
assignedSlot
— If the text is being projected into a Shadow DOM, this property will provide theslot
instance where the text is being projected.wholeText
— A string that contains the current text node along with all adjacent text nodes, concatenated together.
Read more about Text
on MDN.
Comment
It may surprise you that even comments are programmable, but they are. Comment
inherits from CharacterData
and can be created either with new Comment(data)
or document.createComment(data)
.
ASIDE: What’s the benefit of programmable comments? Regardless of what the original designers intended, it turns out that this capability is a critical enabler for many modern JavaScript libraries and frameworks. Why? Rendering engines often need to update parts of the DOM where no element or text exists. An example would be a list of items that needs to render between other text nodes or elements. How does one preserve a stable insertion point when the DOM could be dynamically changing all around the point of insertion? A typical technique is to use a comment node to mark the render location.
Comment
instances are lightweight and don’t render, making them a good fit for location placeholders.
Composing Light and Shadow DOM
Ok, we’ve got all this DOM, but how does it get rendered? Well, that and other related topics like CSS are beyond the scope of this post 😅. However, I would like to recommend a great video to introduce you to rendering: “Chromium University: Life of a Pixel”.
Having said that, I do want to cover a couple of concepts related to visual composition. These are the ideas of the Light DOM and the Shadow DOM.
The Light DOM is what everyone is used to working with. We tend to call it “Light DOM” to distinguish it from the new kid: “Shadow DOM”. The Shadow DOM provides the ability to create isolated DOM subtrees hosted under most HTML elements. The term “Shadow DOM” is a bit unfortunate. I like to think of it as a render DOM whereas I like to think of the “Light DOM” as a semantic DOM. Another way of thinking of things is that the Light DOM is a kind of logical DOM, while the Shadow DOM is a kind of visual DOM.
ASIDE: Those with a background in Xaml will recognize this as the same concept as the Logical Tree and the Visual Tree. Most modern native UI component systems have these concepts under various names.
IMPORTANT: Do not confuse this with React’s Virtual DOM. They are not related, nor do they serve similar purposes.
The neat thing is that I can attach Shadow DOM to almost every element (form elements are the primary exclusions) and if I attach a Shadow DOM to that element, it will then use my Shadow DOM to render that element instead of the browser’s internal user-agent paint algo. Using Declarative Shadow DOM (DSD), I can even do this entirely in HTML with no JavaScript and provide completely encapsulated styles.
NOTE: While the imperative Shadow DOM API is available in every major browser since January 2020, Declarative Shadow DOM is only currently available in Chromium and WebKit browsers. So, if you want to try the example below, be sure to use a browser based on those or use the JavaScript Shadow DOM APIs that are supported everywhere.
For example, if I want the semantics of a p
element but want it to render as a warning, with an icon and text that are styled red, I could accomplish that fully declaratively like this:
<p>
<template shadowrootmode="open">
<style>
:host {
color: red;
fill: currentcolor;
}
</style>
<svg>...</svg><slot></slot>
</template>
This is not a drill!!!
</p>
This HTML will produce a composed runtime DOM structure that looks like this:
<p>
#shadow-root
<style>...</style>
<svg>...</svg><slot><!--This is not a drill!!! visually here.--></slot>
This is not a drill!!!<!--Semantically here.-->
</p>
It will have paragraph element semantics with the svg and styles tucked away in the Shadow DOM, used only for visual purposes. It will visually appear similar to below but remember that only the text will be in the Light DOM, not the svg. It will all be styled red, with the styles also staying encapsulated within the Shadow DOM.
<p>
<svg>...</svg>This is not a drill!!!
</p>
Shadow DOM and the details of light/dom composition enable many things not before possible in HTML. A full treatment of the topic is beyond the scope of this post, but it is important to understand this as a fundamental characteristic of visual composition in all modern HTML and CSS engines.
Events
So far, I’ve talked primarily about structure: how an HTML document is structured and what that translates to in terms of the runtime DOM structure, how it’s visually composed, and how to manipulate it. However, there’s much more going on. While I can’t make an exhaustive exploration of HTML in a single blog post (not even close), there is one more critical topic I want to tackle: Events.
When we began looking at the DOM hierarchy, I noted that underneath everything was EventTarget
. I skipped past this pretty quickly so that we could look at Node
types, but it’s important to circle back and explore this a bit more before we wrap up.
The Key APIs for events that come from EventTarget
are:
addEventListener(type, listener, options)
— Adds alistener
for the specified eventtype
.removeEventListener(type, listener, options)
— Removes alistener
for the specified eventtype
.dispatchEvent(event)
— Dispatches/Publishes the provided event instance.
All this seems pretty self-explanatory, but there are a number of details that are important to clearly grasp. Let’s look into a few of them.
IMPORTANT: Events are synchronously dispatched. This means that after
dispatchEvent(event)
returns, the entire event propagation sequence will have completed. The propagation mechanism does not await async listeners. If it did, besides the general performance implications, the combination of that with a non-passive listener would force the browser to freeze. See below for an explanation of passive listeners.
Listeners
When adding or removing a listener, there are a couple of interesting details worth noting.
First, addEventListener()
attempts to dedupe callbacks. So, if you call the method multiple times with the same listener, you will only have one registration. This is usually desirable. However, if you are creating arrow functions on the fly for listeners, and not storing them for repeat use, each time you do this will result in a different arrow function instance. So, you will get duplicate registrations. Watch out for this.
Second, following on what I’ve just stated about arrow functions, if you want to removeEventListener()
you must pass the same listener that was added. If you are using arrow functions, this means you would have had to have stored the function instance somewhere if you want to properly remove it later.
Third, the listener does not have to be a callback function at all! It can also be an object with an handleEvent()
method.
class MyElement extends HTMLElement {
connectedCallback() {
this.addEventListener("pointerdown", this);
this.addEventListener("keydown", this);
}
handleEvent(event) {
}
}
There are several advantages of using a handler object over a callback function:
- There’s no need to bind the function or use arrow functions, which consume more memory.
- There’s no need to worry about having the proper
this
in yourhandleEvent()
method. - Calling
addEventListener()
with the same listener object multiple times (such as in the case of repeat calls toconnectedCallback()
) will not add multiple listeners. The browser can easily dedupe the references. - If desired, you can handle all the events for your component or experience very easily in a central location.
Event Options
When adding listeners, you can also provide options.
Here are the properties you can include on your options object:
capture
— Indicates that the listener will be invoked during the “capture” phase of the event lifecycle rather than the “bubble” or “target” phase. (See below in “Event Propagation” for more information on event phases.)once
— Indicates that the listener will be invoked the first time the event is dispatched and then automatically removed afterwards.passive
— Tells the browser that your event listener will not callpreventDefault()
. Providing this information to the runtime enables it to improve performance in certain scenarios, particularly around scrolling.signal
— Allows the developer to provide anAbortSignal
during registration. Ifabort()
is signaled, then the listener will be removed.
IMPORTANT: Instead of providing an
options
object, you can also provide a boolean value. If you do, this will be interpreted as a setting forcapture
.
Event Propagation
Having registered and provided the desired options for event listening, it’s critical to understand how events propagate. For the purposes of explanation, let’s assume the following DOM structure:
* window
* document
* html
* body
* section
* div
* icon
* text
Let’s also assume that an event listener for click
is added on the div
and the user has just clicked the text
inside of the div
. Here’s what happens:
- The browser creates a
MouseEvent
and dispatches it from thetext
node, which is known as thetarget
. - The “capture phase” begins as the event travels from the top of the structure down to the target, invoking any
capture
registered listeners along the way. Within each listener,currentTarget
points to the current node.
window => document => html => body => section => div => text
The click event on the div
would not fire in this case though, because it was not registered with the capture
option. If it had been, it would be invoked at this time.
- The event then reaches the
text
node where it will invoke any registered listeners as part of the “target phase”.
NOTE: No event in this scenario will reach the
icon
because it is not in the ancestor path from the target to the document root.
- Next, the event will transition to the “bubble phase” by traveling back up the DOM to the root.
text => div => section => body => html => document => window
- When it reaches the
div
element, our event listener will be invoked.
With this in mind, let’s make sense out of the APIs present on the Event
type itself:
target
— This is the node that the event was dispatched from. In our example above, that is thetext
node where the click occurred. Note that if the real target is inside of a shadow root, then the target will be adjusted to point to the host element. This is called “re-targeting” and will occur every time the event crosses a Shadow Dom boundary.currentTarget
— This is the node that the event listener is invoked for. So, when the listener attached to thediv
is invoked, thecurrentTarget
is thediv
while thetarget
is thetext
.eventPhase
— Indicates which of the three phases the event is currently in. It is an integer value where1
is capture,2
is target, and3
is bubble. If you examine this property within thediv
's click handler, it should have a value of3
.type
— This is the event type, such asclick
orkeydown
.defaultPrevented
— Indicates whether a previous event listener has calledpreventDefault()
on the event to cancel the browser’s default behavior.composedPath()
— Returns an array of nodes that represents the path that the event will traverse from the target to the root. If the real target is inside of a closed shadow root, then the composed path will begin from the host element of the shadow root.stopPropagation()
— This stops the propagation of the event up/down the DOM. However, if there is more than one listener for the event on the current target, the additional handlers will still be invoked. Also, the default behavior for the event, if one exists, will still be invoked.stopImmediatePropagation()
— This stops the propagation of the event up/down the DOM. It also prevents any additional listeners on the current target from being invoked. However, the default behavior for the event, if one exists, will still be invoked.preventDefault()
— If the event has a default behavior, calling this method will prevent it from being invoked. However, it will have no effect on the propagation of the event itself. It also has no effect if the listener ispassive
or the event is notcancelable
.
There are a number of additional APIs on the base Event
class, but these are the ones I’ve used the most. To learn more about the additional functionality of the event as well as functionality specific to the various event types, see MDN.
IMPORTANT: Not all events bubble. Some stop when they get to the
target
. Here’s a helpful event reference which indicates which events bubble, and which are cancelable.
Custom Events
I’ve spoken a good bit about creating custom HTML elements in the past on my blog. But did you know you can create your own events as well? To do so, you instantiate aCustomEvent
and provide your event type, its data details, and its options. Then you dispatch it like any other event.
const myEvent = new CustomEvent("my-event", {
detail: {
anyData: "you want"
},
bubbles: true,
composed: true,
cancelable: true
});
node.dispatchEvent(myEvent);
Here’s an explanation of the options:
detail
— Any data specific to your event that you want to include.bubbles
— Whether or not your event should participate in the bubble phase or stop after the target phase.composed
— Indicates whether your event should propagate across shadow root boundaries if it bubbles.cancelable
— Indicates whether or not your event’s default behavior can be cancelled by a call topreventDefault()
.
NOTE: You can also create a custom class that inherits from
CustomEvent
and dispatch that.
Wrapping Up
While I’ve worked extensively with HTML and the DOM for many years, I learned a few new things myself while putting together this article. I hope you learned something new as well and that it will empower you on your journey to create quality, accessible, and performant web solutions.
If you enjoyed this article, you might want to check out my Web Component Engineering course. I’d also love it if you would subscribe to this blog, subscribe to my YouTube channel, or follow me on twitter. Your support greatly helps me continue writing and bringing this kind of content to the broader community. Thank you!