Web Components Demystified

Scott Jehl released a course called Web Components Demystified. This is my full set of notes from Scott's course. You'll still want to take the course on your own, and I encourage you to because Scott is an excellent teacher who makes all of this stuff extremely accessible, even to noobs like me. Web Components Demystified originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Mar 26, 2025 - 22:58
 0  18
Web Components Demystified

Scott Jehl released a course called Web Components Demystified. I love that name because it says what the course is about right on the tin: you’re going to learn about web components and clear up any confusion you may already have about them.

And there’s plenty of confusion to go around! “Components” is already a loaded term that’s come to mean everything from a piece of UI, like a search component, to an element you can drop in and reuse anywhere, such as a React component. The web is chock-full of components, tell you what.

But what we’re talking about here is a set of standards where HTML, CSS, and JavaScript rally together so that we can create custom elements that behave exactly how we want them to. It’s how we can make an element called and the browser knows what to do with it.

This is my full set of notes from Scott’s course. I wouldn’t say they’re complete or even a direct one-to-one replacement for watching the course. You’ll still want to do that on your own, and I encourage you to because Scott is an excellent teacher who makes all of this stuff extremely accessible, even to noobs like me.

Chapter 1: What Web Components Are… and Aren’t

Web components are not built-in elements, even though that’s what they might look like at first glance. Rather, they are a set of technologies that allow us to instruct what the element is and how it behaves. Think of it the same way that “responsive web design” is not a thing but rather a set of strategies for adapting design to different web contexts. So, just as responsive web design is a set of ingredients — including media fluid grids, flexible images, and media queries — web components are a concoction involving:

Custom elements

These are HTML elements that are not built into the browser. We make them up. They include a letter and a dash.


  Hey, I'm Fancy

We’ll go over these in greater detail in the next module.

HTML templates

Templates are bits of reusable markup that generate more markup. We can hide something until we make use of it.

Much more on this in the third module.

Shadow DOM

The DOM is queryable.

document.querySelector("h1");
// 

Hello, World

The Shadow DOM is a fragment of the DOM where markup, scripts, and styles are encapsulated from other DOM elements. We’ll cover this in the fourth module, including how to content.

There used to be a fourth “ingredient” called HTML Imports, but those have been nixed.

In short, web components might be called “components” but they aren’t really components more than technologies. In React, components sort of work like partials. It defines a snippet of HTML that you drop into your code and it outputs in the DOM. Web Components are built off of HTML Elements. They are not replaced when rendered the way they are in JavaScript component frameworks. Web components are quite literally HTML elements and have to obey HTML rules. For example:


We’re generating meaningful HTML up-front rather than rendering it in the browser through the client after the fact. Provide the markup and enhance it! Web components have been around a while now, even if it seems we’re only starting to talk about them now.

Chapter 2: Custom Elements

First off, custom elements are not built-in HTML elements. We instruct what they are and how they behave. They are named with a dash and at must contain least one letter. All of the following are valid names for custom elements:

Just remember that there are some reserved names for MathML and SVG elements, like . Also, they cannot be void elements, e.g. , meaning they have to have a correspoonding closing tag.

Since custom elements are not built-in elements, they are undefined by default — and being undefined can be a useful thing! That means we can use them as containers with default properties. For example, they are display: inline by default and inherit the current font-family, which can be useful to pass down to the contents. We can also use them as styling hooks since they can be selected in CSS. Or maybe they can be used for accessibility hints. The bottom line is that they do not require JavaScript in order to make them immediately useful.

Working with JavaScript. If there is one  on the page, we can query it and set a click handler on it with an event listener. But if we were to insert more instances on the page later, we would need to query it when it’s appended and re-run the function since it is not part of the original document rendering.

Defining a custom element

This defines and registers the custom element. It teaches the browser that this is an instance of the Custom Elements API and extends the same class that makes other HTML elements valid HTML elements:

My Element

Check out the methods we get immediate access to:

Showing the prototype methods and properties of a custom element in DevTools, including define, get, getName, upgrade, and whenDefined.

Breaking down the syntax

customElements
  .define(
    "my-element",
    class extends HTMLElement {}
  );
	
// Functionally the same as:
class MyElement extends HTMLElement {}
customElements.define("my-element", MyElement);
export default myElement

// ...which makes it importable by other elements:
import MyElement from './MyElement.js';
const myElement = new MyElement();
document.body.appendChild(myElement);

// 
//   
// 

// Or simply pull it into a page
// Don't need to `export default` but it doesn't hurt to leave it
// My Element
// 

It’s possible to define a custom element by extending a specific HTML element. The specification documents this, but Scott is focusing on the primary way.

class WordCount extends HTMLParagraphElement
customElements.define("word-count", WordCount, { extends: "p" });

// 

This is a custom paragraph!

Scott says do not use this because WebKit is not going to implement it. We would have to polyfill it forever, or as long as WebKit holds out. Consider it a dead end.

The lifecycle

A component has various moments in its “life” span:

  • Constructed (constructor)
  • Connected (connectedCallback)
  • Adopted (adoptedCallback)
  • Attribute Changed (attributeChangedCallback)
  • Disconnected (disconnectedCallback)

We can hook into these to define the element’s behavior.

class myElement extends HTMLElement {
  constructor() {}
  connectedCallback() {}
  adoptedCallback() {}
  attributeChangedCallback() {}
  disconnectedCallback() {}
}

customElements.define("my-element", MyElement);

constructor()

class myElement extends HTMLElement {
  constructor() {
    // provides us with the `this` keyword
    super()
    
    // add a property
    this.someProperty = "Some value goes here";
    // add event listener
    this.addEventListener("click", () => {});
  }
}

customElements.define("my-element", MyElement);

“When the constructor is called, do this…” We don’t have to have a constructor when working with custom elements, but if we do, then we need to call super() because we’re extending another class and we’ll get all of those properties.

Constructor is useful, but not for a lot of things. It’s useful for setting up initial state, registering default properties, adding event listeners, and even creating Shadow DOM (which Scott will get into in a later module). For example, we are unable to sniff out whether or not the custom element is in another element because we don’t know anything about its parent container yet (that’s where other lifecycle methods come into play) — we’ve merely defined it.

connectedCallback()

class myElement extends HTMLElement {
  // the constructor is unnecessary in this example but doesn't hurt.
  constructor() {
    super()
  }
  // let me know when my element has been found on the page.
  connectedCallback() {
    console.log(`${this.nodeName} was added to the page.`);
  }
}

customElements.define("my-element", MyElement);

Note that there is some strangeness when it comes to timing things. Sometimes isConnected returns true during the constructor. connectedCallback() is our best way to know when the component is found on the page. This is the moment it is connected to the DOM. Use it to attach event listeners.

If the 

A few more custom element methods:

customElements.get('my-element');
// returns MyElement Class

customElements.getName(MyElement);
// returns 'my-element'

customElements.whenDefined("my-element");
// waits for custom element to be defined

const el = document.createElement("spider-man");
class SpiderMan extends HTMLElement {
  constructor() {
    super();
    console.log("constructor!!");
  }
}
customElements.define("spider-man", SpiderMan);

customElements.upgrade(el);
// returns "constructor!!"

Custom methods and events:



Bring your own base class, in the same way web components frameworks like Lit do:

class BaseElement extends HTMLElement {
  $ = this.querySelector;
}
// extend the base, use its helper
class myElement extends BaseElement {
  firstLi = this.$("li");
}

Practice prompt

Create a custom HTML element called  that displays the text “Hi, World!” when added to the page:

CodePen Embed Fallback

Enhance the element to accept a name attribute, displaying "Hi, [Name]!" instead:

CodePen Embed Fallback

Chapter 3: HTML Templates

The