Introduction to Web Components
Ever since I first learned about web components I have been kind of fascinated by them and I’m not entirely sure why. Sure, there is something “neat” and elegant about the idea of creating components - building blocks of the web if you will - outside of a mainstream JavaScript framework, using just web standards. And it’s cool that it happens to have a spec called the “shadow DOM” which sounds like every movie villain’s favourite web development tool.
However, it’s the fact that you can build your own freaking HTML tags completely from scratch and encapsulate them that really drew me to this technology. These things are absolutely perfect for Design Systems and other component libraries that require interoperability between projects built in different JavaScript frameworks. React? Vue? Svelte? No problem - web components can handle them all.
Want to know more? Alright, let’s go!
If you want to follow the code examples in this post, I have made a Codepen collection containing all the code for your perusal
What makes a “web component”?
The term “web component” actually refers to technology that draws from several different specifications:
Custom elements - this is the bit that allows you to define your own HTML tags and is the most important spec of the three; without defining a custom element, you don’t really have a web component
The shadow DOM - the one with the cool-sounding name; this allows you to encapsulate your tag (i.e. hide its inner workings from users), making sure your tag will render as you intended it to with no interference
HTML templates - these are simply code blocks that you define at build time using a
<template>
tag and then do something with at runtime; usually plug in some data
As mentioned, custom elements are the most important part of web components - without them, you don’t really have a component in any kind of functional way (some people say that web components should really be called “custom elements” and I can see why). The other specs are bolt-ons that add further functionality, but they are not essential; they’re simply tools you can reach for should you need them.
Let’s explore these different specs further.
Custom elements
All custom elements start with a class
defined using JavaScript. Say we wanted to create an HTML tag for book summaries - imagine we're running a book review website. We’d start off with something like this:
class BookSummary extends HTMLElement {
constructor() {
super();
alert('This is my excellent book summary component');
}
}
Now, you can either extend this class from HTMLElement
like we did above, which means you have to implement all of its behaviour from scratch, or you can extend from existing elements like HTMLParagraphElement
or HTMLImageElement
. The latter means you get certain behaviours for free because you’re essentially saying “I’m making a custom element that’s kind of like a paragraph, give me a starter for ten”.
Custom elements have lifecycle methods you can hook into that will allow you to call certain logic when an element is added to or removed from a page, moved somewhere else or when its attributes change. These are beyond the scope of this blog post but I want to write a tutorial that shows the exact steps to create a web component soon, so do keep an eye out for that.
In order to actually use a custom element, we have to register it - this will effectively make the browser aware that the tag exists. This is done as follows:
customElements.define('book-summary', BookSummary);
Note that you have to include a dash (-
) in the name of your custom element - this is to make sure it doesn’t clash with a tag that already exists. Once that is all done, you can display your custom element on the page by adding the following HTML to your document:
<book-summary />
How neat is that?! Obviously, our custom element doesn’t really do much (except display an annoying alert), but you can see how you could expand this to do more.
There is a Codepen containing this excellent book summary component, just in case you wanted to see that it actually works.
The shadow DOM
This is simultaneously the most interesting and most controversial part of the web components spec. The shadow DOM allows you to encapsulate styles and scripts inside your custom element - this is important because custom elements are designed to be used in all sorts of places and as the component’s creator, you don’t know what the conditions will be like when your little component get used. Using the shadow DOM means styles and scripts defined in the website using your web component will not affect its innards and vice versa (with a few exceptions).
Let’s go back to our <book-summary>
example. There are two main ways to create a shadow DOM - the first is “imperatively” (see below) with JavaScript, as follows:
class BookSummary extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = 'This is my excellent book summary component';
}
}
You see that little this.attachShadow()
call? That’s what enables us to use the shadow DOM. This time, instead of calling that pesky alert()
, we’re setting some HTML inside the shadow DOM.
I have created a Codepen showing a custom element with a imperative shadow DOM in case you wanted to have a play with this.
Alternatively, you can now (since August 2024) add a “declarative” (see below) shadow DOM using HTML:
<book-summary>
<template shadowrootmode="closed">
<span>This is my excellent book summary component</span>
</template>
</book-summary>
This time, we have to edit our HTML rather than our JavaScript. The big advantage of doing things this way is that this HTML will load on the server, which means the end user doesn’t have to wait until JavaScript loads in to be able to see this content. This is good for search engine optimisation (SEO) and performance.
I have created a Codepen showing a custom element with a declarative shadow DOM in case you wanted to have a play with this.
Shadow DOM modes
There are two modes for the shadow DOM:
Open - this mode will let the JavaScript in your app (even if it’s not inside the custom element) access the internals of your web component using the
shadowRoot
property. This effectively no longer makes the component encapsulated! But there may be times when you would want thisClosed - this will keep the internals of your custom element hidden from all the other code in your application, which is a big benefit, but can cause issues precisely because it is difficult to access anything inside your custom element
Why is the shadow DOM controversial?
There is a lot of hate (or at best apathy) for the shadow DOM online, with many folks not really considering it all that useful. It does have its drawbacks, quite honestly, especially if you’re using the imperative closed shadow DOM.
Encapsulating your code from users also encapsulates it from useful tools, including things like screen readers and accessibility testing platforms. This will improve as support for the technology grows, but has historically been a problem.
If you’re using the imperative shadow DOM, any content inside the custom element will only load in once JavaScript has initialised, so there is a risk that there will be a delay between the web page rendering and the custom element showing up. This is generally not a good user experience so it’s always best to use the declarative shadow DOM if you can.
What are words: imperative vs declarative
If you’re wondering what these terms mean, this section is for you!
You honestly don’t have to worry about this too much, but if you’re like me and get a bit hung up on definitions of words, you can think of “imperative” as writing out exactly how something should be done whereas “declarative” is just saying what you want and trusting the programming language itself to take care of it.
A good analogy is if you were to ask someone for a cup of tea. Imperatively, you’d say:
Fill the kettle with water and put it on to boil, then get a mug from the shelf and put a teabag in it. Fill it with hot water once the kettle has finished boiling, then let the teabag steep for five minutes. Take the teabag out, then add milk.
Declaratively, you’d just say:
Bring me a cup of tea
In practice, you just need to have a vague sense of what these terms mean, you don’t need to be too concerned about the details.
HTML templates (and slots)
An HTML template is simply a way to declare a re-usable building block:
<template id="my-text">
<span>This is my excellent book summary component</span>
</template>
In your custom element, you would obtain a reference to this template using the ID and then you can manipulate it however you want:
const template = document.getElementById("my-text").content;
this.appendChild(template);
In this case, we’re just adding the template to our element. I have created a Codepen showing a custom element with a template in case you wanted to have a play with this.
Templates so far are a bit underwhelming - all you can do is grab a copy of some text and re-use it. What if we could make them a bit more flexible and “inject” different things into them? That’s what slots are for!
Slots only work using the shadow DOM, so we have to do a bit of extra finagling to get them to work. In our HTML, we have to declare the following:
<template id="my-text">
<slot name="book-summary">This is the default summary</slot>
</template>
<book-summary>
<span slot="book-summary">This is my excellent book summary component</span>
</book-summary>
The template now has a <slot>
element in it with a pre-defined name and the <book-summary>
custom element defines what should go in that slot by adding a <span>
(this can be any tag though!) with a slot
attribute that matches the name
of the slot in the template. If you were to leave off the <span>
tag, the text in that slot would fall back to the default value of “This is the default summary”.
Then, in our JavaScript, we connect it all up:
class BookSummary extends HTMLElement {
constructor() {
super();
const templateContent = document.getElementById("my-text").content;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.appendChild(templateContent.cloneNode(true));
}
}
customElements.define('book-summary', BookSummary);
Just by virtue of attaching a shadow root and using the template, the slot will magically replace the default text with the text defined in the <span>
. Hopefully you can begin to see how this could end up being useful!
I have created a Codepen showing a custom element with a template and slots in case you wanted to have a play with this.
Conclusion
Web components are fun and interesting to play with and despite their slightly tarnished reputation, the standard is improving all the time. Big companies use them in their design systems, so I don’t think they’re going away any time soon and they’re worthwhile exploring if you haven’t already.
Finding out more about the different specifications that web components use is handy, but a real-world example would be even more so. I will be writing a blog post showing a fully functional custom element soon, so stay tuned!
As always, if you have any questions, do feel free to comment and I’ll do my best to answer them
Subscribe to my newsletter
Read articles from Dana Ciocan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Dana Ciocan
Dana Ciocan
I'm a Staff Engineer working at The Economist. I love diving deep into big problems and surfacing with a workable solution. I also love making my own garments, cooking, crafting and gardening.