Just like any technology, Web Components also have a set of specifications that need to be followed in order to achieve the functionality associated with them.
A Web Component specification has the following parts:
- Custom element: The ability to create custom HTML tags and make sure that the browser understands how to use this HTML tag
- Shadow DOM: The ability to encapsulate the contents of the component from other parts of the DOM
- Template: Being able to create a reusable DOM structure that can be modified on the fly and output desired results
These three specifications, when used together, provide a way to create a custom HTML tag that can output desired results (DOM structure) and let it encapsulate from the page, making it reusable again and again.
Now that we know these specifications and what they do, let's dive into them individually and try to look at their JavaScript APIs.
A custom element specification lets you create a custom HTML tag that can be used as its own HTML tag on the page. In order to achieve this, we need to first write a class with the functionalities associated with that HTML element, and then we need to register it so that the browser understands what this HTML tag is, and how it can be used on the page.
If you are someone who is new to the concept of classes in JavaScript, here is how you can create a class:
Pretty simple, right? Let's use the same class structure to create our custom element class, say HelloWorld:
In the preceding code, our custom element class is called HelloWorld and it is extending interface from the HTMLElement class, which represents how an HTML element should work on a page. So, HelloWorld now knows what click events are, what CSS is, and so on, simply by extending HTMLElement.
Inside this class, we have the constructor() method, which gets called as soon as a new instance of this class is created. The super() function needs to be called in order to correctly instantiate the properties of the extended class.
The preceding code simply creates the element class definition. We still need to register this element. We can do so by writing the following code:
What it does is register the class HelloWorld by defining it using the define() interface in the customElements interface; hello-world is the name of the custom element that is going to be available on the page.
Once this is defined, you can used the custom element by simply writing the HTML tag as following:
When this code is run on a browser, it will render the text, Hello World.
This is the second specification for Web Components and this one is responsible for encapsulation. Both the CSS and DOM can be encapsulated so that they are hidden from the rest of the page. What a shadow DOM does is let you create a new root node, called shadow root, that is hidden from the normal DOM of the page.
However, even before we jump into the concept of a shadow DOM, let's try to look at what a normal DOM looks like. Any page with a DOM follows a tree structure. Here I have the DOM structure of a very simple page:

In the preceding image, you can see that #document is the root node for this page.
You can find out the root node of a page by typing document.querySelector('html').getRootNode() in the browser console.
If you try to get the child nodes of an HTML tag using document.querySelector('html').childNodes in the browser console, then you can see the following screenshot:

This shows that the DOM follows a tree structure. You can go deeper into these nodes by clicking on the arrow right next to the name of the node. And just like how I have shown in the screenshot, anyone can go into a particular node by expanding it and change these values. In order to achieve this encapsulation, the concept of a shadow DOM was invented.
What a shadow DOM does is let you create a new root node, called shadow root, that is hidden from the normal DOM of the page. This shadow root can have any HTML inside and can work as any normal HTML DOM structure with events and CSS. But this shadow root can only be accessed by a shadow host attached to the DOM.
For example, let's say that instead of having text inside the <p> tag in the preceding example, we have a shadow host that is attached to a shadow root. This is what the page source would look like:

Furthermore, if you tried to get the child nodes of this <p> tag, you would see something like this:

Notice that there is a <span> tag in the shadow root. Even though this <span> tag is present inside the <p> tag, the shadow root does not let JavaScript APIs touch it. This is how the shadow DOM encapsulates code inside itself.
Now that we know what a shadow DOM does, let's jump on to some code and learn how to create our own shadow DOMs.
Let's say we have a DOM with a class name entry. This is what it looks like:
In order to create a shadow DOM in this div, we will first need to get the reference to this .entry div, then we need to mark it as a shadow root, and then append the content to this shadow root. So, here is the JavaScript code for creating a shadowRoot inside a div and adding contents to it:
Pretty simple, right? Remember, we are still discussing the shadow DOM spec. We haven't started implementing it inside a custom element yet. Let's recall the definition of our hello-world custom element. This is what it looked like:
Notice that the text Hello World is currently being added to normal DOM. We can use the same shadow DOM concepts discussed earlier in this custom element.
First, we need to get the reference to the node where we want to attach the shadow root. In this case, let's make the custom element itself the shadow host by using the following code:
Now, we can either add a text node or create a new element and append it to this shadowRoot:
Till now, we have only created custom elements and shadow DOMs that require only one or, at the most, two lines of HTML code. If we move on to a real-life example, HTML code can be more than two lines. It can start from a few lines of nested div to images and paragraphs—you get the picture. The template specification provides a way to hold HTML on the browser without actually rendering it on the page. Let us look at a small example of a template:
You can write a template inside <template> tags and assign it an identifier, just as I have done with the help of an id. You can put it anywhere on the page; it does not matter. We can get its content with the help of JavaScript APIs and then clone it and put it inside any DOM, just as I have shown in the following:
Similarly, we can have any number of templates on the page, which can be used by any JavaScript code.
Let's now use the same template with a shadow DOM. We will keep the template as it is. The changes in the JavaScript code would be something like this:
We are doing the same thing that we did in the previous example, but, instead of appending the code directly to the target div, we are first attaching a shadow root to the target div, and then appending the cloned template content.
We should be able to use the exact same concept inside the autonomous custom element that uses a shadow DOM. Let's give it a try.
Let's edit the id of the template and call it hello-world-template:
We will follow the exact same approach that we followed in the preceding example. We will get the template content from the template reference, clone it, and append it in the custom element, making the code of the custom element look like the following:
Now we can simply call the HTML tag inside our page using the following code:
If we inspect the DOM structure inside developer tools, this is what we see:

Module loader API is not a part of Web Component spec sheet, but it is definitely something that is useful to know when it comes to creating and using multiple classes. As the name says, this specification lets a user load the modules. That is, if you have a bunch of classes, you can use module loaders to load these classes into the web page.
If your build process involves using WebPack or Gulp or anything else that lets you import modules directly or indirectly, please feel free to skip this section.
Let's start with the basics. Let's say we have our index.html like this:
We can see that there is a <p> tag in this HTML file. Now, let's say we have a class called AddNumber, whose purpose is to add a random number between 0 and 1 to this <p> tag. This would make the code look something like this:
Simple, right? If you open the page on a browser, you will simply see a random number, and if you inspect the page, you will see that the random number replaced the text which was inside the <p> tag.
If we choose to store it in a JavaScript file, we can try to import it using the following code, where addNumber.js is the name of the file:
Now, let's say you have a randomNumberGenerator function instead of the Math.random() method. The code would look something like this:
We also want the ability to let the user create a new object of the AddNumber class, rather than us creating it in the file. We do not want the user to know how randomNumberGenerator works, so we want the user to be only able to create the object of AddNumber. This way, we reach how modules work. We, the creators of modules, decide which functionalities the user can use and which they cannot.
We can choose what the user can use with the help of the export keyword. This would make the code look something like this:
When this file is imported (note that we haven't talked about imports yet), the user will only be able to use the AddNumber class. The randomNumberGenerator function won't be available to the user.
Similarly, if you have another file with, say, two other functions, add() and subtract(), you can export both of them as shown in the following:
Importing a module can be easily done with the help of the import keyword. In this section, we will talk about the type="module" attribute.
Inside the HTML file, index.html, instead of type=text/javascript, we can use type=module to tell the browser that the file that we are importing is a module. This is what it will look like when we are trying to import the file addNumber.js:
This is how it will look if we import functions from the calc.js module:
Notice how we can change the name of the module exported from AddNumber, which uses export default, and how we have to use the same name as the name of the function exported using export.
In the previous examples, that is, addNumber.js and calc.js, we saw that there are two ways to export something: export and export default. The simplest way to understand it is as follows: when a file exports multiple things with different names and when these names cannot be changed after import, it is a named export, whereas, when we export only one thing from a module file and this name can be changed to anything after the import, it is a default export.
Let's say we need to create a Web Component that does a very simple task of showing a heading and a paragraph inside it, and the name of the custom element should be <revamped-paragraph>. This is what the definition of this Web Component would look like:
Our index.html file, the file that imports this module, would look like this:
Notice how the template is a part of our HTML, and how it gets used when the module is imported. We will be learning about all the steps that take place from the actual registration of the Web Components to what happens when they are removed from the page in the next chapter, where we will learn about life cycle methods. But for now, we need to look at more examples to understand how to create Web Components.
Let's take a look at another example. In this example, we need to import multiple Web Components in the index.html file. The components are as follows:
- A student attendance table component: A table that shows the index number, student name, and attendance in a checkbox. This data is obtained from a student.json file.
- An information banner component: The purpose of this component is to show a phone number and an address for the school where these students are studying.
- A time slot component: A component that lets the user select a time slot for the class between three sets of timings.
Let us start with the first one, the <student-attendance-table> component. We need to first identify what it needs. In my opinion, these are the things it needs:
- A fetch call to the student.json file
- A template for each row of the string. I will be using template strings here
- A default text that says loading... when it is making the call and another text that says unable to retrieve student list. when the fetch call fails
This is what our student.json file looks like:
This is what the definition of the Web Component looks like:
Notice the functions getLoadingText() and getErrorText(). Their purpose is simply to return a text. Then the render() method first consumes the getLoadingText() method, and then makes the call using getStudentList() to fetch the student list from student.json file.
Once this student list is fetched, it gets passed onto generateTable() method, where every name and its index is passed into the getTableRow() method to generate rows and then gets returned back to be a part of the table. Once the table is formed, it is then passed into the appendHTMLToShadowDOM() method to be added to the shadow DOM for the component.
It's time to look into the <information-banner> component. Since this component simply needs to display a phone number and an address of the school where they are studying, we can use <template> and make it work. This is what it looks like:
Furthermore, information-banner-template looks something like this:
As you can see, it is not much different than the custom elements we have already talked about in previous sections.
Let's move on to the last custom element, the <time-slot> component. Since it also involves a preset number of time slots, we can use a <template> tag to do our work.
The template would look something like this:
The definition of the <time-slot> component would look like this:
It is the same as the previous component.
Now that we have written the Web Components, it's time to take a look at the index.html file that includes all of these components together. This is what it looks like:
As you can see, one <script> tag of type="module" can import all three of them together, and can register the custom elements, which can be used in the <body> tag.