A Few DOM Reminders

EisenbergEffect
23 min readJul 24, 2023

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 an HTMLUnkownElement).
  • Elements can have attributes, such as lang shown above. A Content Attribute has a name and a value in the form name="value". A Boolean Attribute has just a name, such as disabled. 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, the meta element declares the document’s character encoding with the charset attribute.
  • title — A document should always have a title 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. The rel attribute describes the nature of the relationship and the href indicates where to find the resource. The optional type 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 the title. 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 and link 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 return null if there is no node in that relationship.
  • isConnected — A boolean 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 returns false.
  • 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 of 1 while a text node will have a value of 3.
  • 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. Pass true to deeply clone the entire subtree.

Check out the rest of the NodeAPIs 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 on Element instances but not Node 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 the Attr nodes on the element.
  • children — A list of all the child elements. Do not confuse this with Node#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 return null if there is no element in that relationship. This doesn’t mean that there isn’t a non-element Node in that relationship though. e.g. The previous node might be Text.

Metadata

  • assignedSlot — If the DOM element is being projected into a Shadow DOM, this property will provide the slot instance where the element is being projected.
  • classList and className — Used to both read and write classes associated with the element. classList is an instance of DOMTokenList, 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 to classList, this is an instance of DOMTokenList, with a rich API for manipulating parts.
  • querySelector(selector) and querySelectorAll(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 to tagName but lowercase and without any xml prefix (if relevant).

Manipulation

  • aria* — Many ARIA related properties can be used on Element 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 the setHTML(...) API where it’s supported.
  • insertAdjacentElement(position, element), insertAdjacentHTML(position, htmlText), and insertAdjacentText(position, charData) — Inserts the respective node types or HTML strings according to the specified position relative to the current element. The position 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 be null.
  • getAttribute(name), setAttribute(name, value), hasAttribute(name), removeAttribute(name), and toggleAttribute(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) on Element and appendChild(node) on Node? An obvious difference is that append() accepts multiple nodes while appendChild() only accepts one. However, an additional difference is that append() also accepts strings, which it will automatically turn into Text 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’s data- attributes.
  • dir — Indicates the directionality of the element.
  • hidden — Indicates whether the hidden attribute is present.
  • lang — Indicates the language of the element and its contents.

Manipulation

  • attachInternals() — Used from within a Web Component to initialize the ElementInternals 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 string true or false.
  • draggable — Makes the element draggable.
  • focus() — Directs keyboard focus to the current element.
  • style — Enables manipulating the element’s styles via an instance of CSSStyleDeclaration.
  • 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 of HTMLParagraphElement, but a <section> element will be an instance of HTMLElement. 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 by HTMLElement 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 — The Element 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 or false. e.g. hidden
  • IDL Attributes — These aren’t really “attributes” at all and won’t be in the attributes collection on Element. 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 the htmlFor IDL Attribute on HTMLLabelElement. 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 the input's value 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's value property write back to its value 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 the value Content Attribute for this. If the value 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 entire CharacterData 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 the slot 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 a listener for the specified event type.
  • removeEventListener(type, listener, options) — Removes a listener for the specified event type.
  • 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 your handleEvent() method.
  • Calling addEventListener() with the same listener object multiple times (such as in the case of repeat calls to connectedCallback()) 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 call preventDefault(). Providing this information to the runtime enables it to improve performance in certain scenarios, particularly around scrolling.
  • signal — Allows the developer to provide an AbortSignal during registration. If abort() 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 for capture.

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 the target.
  • 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 the text 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 the div is invoked, the currentTarget is the div while the target is the text.
  • eventPhase — Indicates which of the three phases the event is currently in. It is an integer value where 1 is capture, 2 is target, and 3 is bubble. If you examine this property within the div's click handler, it should have a value of 3.
  • type — This is the event type, such as click or keydown.
  • defaultPrevented — Indicates whether a previous event listener has called preventDefault() 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 is passive or the event is not cancelable.

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 to preventDefault().

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!

--

--

EisenbergEffect

W3C, WICG & TC39 contributor focused on Web Standards, UI Architecture & Engineering Culture. Former Technical Fellow, Principal Architect & VP-level tech lead.