Welcome back to the Web Components 101 Series! We're going to discuss the state of Web Components, provide expert advice, give tips and tricks and reveal the inner workings of Web Components.
In today's tutorial, we're going to teach you the fundamentals of Web Components by building a <name-tag>
component step by step!
First, we have to learn the rules. Then, we're going to set up our development environment.
Next, we'll define a new HTML element, going to learn how to pass attributes, create and use the Shadow DOM, and use HTML templates.
About the author
Stefan is a JavaScript Web Developer with more than 10 years of experience. He loves to play sports, read books and occasionally jump out of planes (with a parachute that is).
☞ If you like this article, please support me by buying me a coffee ❤️.
Other posts in the Web Components 101 series
- What are Web Components?
- Why use Web Components?
- [Tutorial] How to create a Web Component? (this post)
The basic rules
Even Web Components have basic rules and if we play by them, the possibilities are endless! We can even include emojis or non-Latin characters into the names, like <animal-😺>
and <char-ッ>
.
These are the rules:
- You can't register a Custom Element more than once.
- Custom Elements cannot be self-closing.
- To prevent name clashing with existing HTML elements, valid names should:
- Always include a hyphen (-) in its name.
- Always be lower case.
- Not contain any uppercase characters.
Setting up our development environment
For this tutorial, we're going to use the Components IDE from the good folks at WebComponents.dev. No set up required! Everything is already in place and properly configured, so we can start developing our component straight away. It even comes with Storybook and Mocha preinstalled and preconfigured.
Steps to set up our dev env
- Go to the Components IDE
- Click the Fork button in the top right of the screen to create your copy.
- Profit! Your environment is set up successfully.
Defining a new HTML Element
Let's have a look at index.html
.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
highlight-next-line
<script src="./dist/name-tag.js" type="module"></script>
</head>
<body>
<h3>Hello World</h3>
highlight-next-line
<name-tag></name-tag>
</body>
</html>
On line 5, we include our component with a <script>
. This allows us to use our component, just like any other HTML elements in the <body>
(line 10) of our page.
But we don't see anything yet, our page is empty. This is because our name tag isn't a proper HTML Tag (yet). We have to define a new HTML Element and this is done with JavaScript.
- Open
name-tag.js
and create a class that extends the base HTMLElement class. - Call
super()
in the class constructor. Super sets and returns the component'sthis
scope and ensures that the right property chain is inherited. - Register our element to the Custom Elements Registry to teach the browser about our new component.
This is how our class should look like:
class UserCard extends HTMLElement {
constructor() {
super();
}
}
customElements.define('name-tag', UserCard);
Congrats! You've successfully created and registered a new HTML Tag!
Passing values to the component with HTML attributes
Our name tag doesn't do anything interesting yet. Let's change that and display the user's name, that we pass to the component with a name
attribute.
Attributes provide extra information about HTML tags, always come in name/value pairs, and could only contain strings.
First, we have to add a name
attribute to the <name-tag>
in index.html. This enables us to pass and read the value from our component
<name-tag name="John Doe"></name-tag>
Now that we've passed the attribute, it's time to retrieve it! We do this with the Element.getAttribute() method that we add to the components constructor()
.
Finally, we're able to push the attribute's value to the components inner HTML. Let's wrap it between a <h3>
.
This is how our components class should look like:
class UserCard extends HTMLElement {
constructor() {
super();
this.innerHTML = `<h3>${this.getAttribute('name')}</h3>`;
}
}
...
Our component now outputs "John Doe".
Add global styling
Let's add some global styling to see what happens.
Add the following CSS to the <head>
in index.html
and see that the component's heading color changes to Rebecca purple:
<style>
h3 {
color: rebeccapurple;
}
</style>
Create and use the Shadow DOM
Now it's time to get the Shadow DOM involved! This ensures the encapsulation of our element and prevents CSS and JavaScript from leaking in and out.
- Add
this.attachShadow({mode: 'open'});
to the component's constructor (read more about Shadow DOM modes here). - We also have to attach our
innerHTML
to the shadow root. Replacethis.innerHTML
withthis.shadowRoot.innerHTML
.
Here is the diff of our constructor:
...
constructor() {
super();
this.attachShadow({mode: 'open'});
- this.innerHTML = `<h3>${this.getAttribute('name')}</h3>`;
+ this.shadowRoot.innerHTML = `<h3>${this.getAttribute('name')}</h3>`;
}
...
Notice that the global styling isn't affecting our component anymore. The Shadow DOM is successfully attached and our component is successfully encapsulated.
Create and use HTML Templates
The next step is to create and use HTML Templates.
First, we have to create a const template
outside our components class in name-tag.js, create a new template element with the Document.createElement() method and assign it to our const.
const template = document.createElement('template');
template.innerHTML = `
<style>
h3 {
color: darkolivegreen; //because I LOVE olives
}
</style>
<div class="name-tag">
<h3></h3>
</div>
`;
With the template in place, we're able to clone it to the components Shadow Root. We have to replace our previous "HTML Template" solution.
...
class UserCard extends HTMLElement {
constructor(){
super();
this.attachShadow({mode: 'open'});
- this.shadowRoot.innerHTML = `<h3>${this.getAttribute('name')}</h3>`;
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}
...
What about passing attributes?!
Although we've added some styles, we see a blank page again. Our attribute values aren't rendered, so let's change that.
We have to get out attribute's value to the template somehow. We don't have direct access to the components scope in the template, so we have to do it differently.
<div class="name-tag">
<h3>${this.getAttribute('name')}</h3>
</div>
This won't work since we don't have access to the component's scope in the template.
We have to query the Shadow DOM for the desired HTML Element (i.e. <h3>
) and push the value of the attribute to its inner HTML.
constructior() {
...
this.shadowRoot.querySelector('h3').innerText = this.getAttribute('name');
}
The result is that we see "John Doe" again on our page and this time, it's colored differently and the heading on the main page stays Rebecca purple! The styling we've applied works like a charm and is contained to the Shadow DOM. Just like we wanted to: No leaking of styles thanks to our component's encapsulating properties.
Bonus: Update styles
Update the <style>
in the template to make our component look a bit more appealing:
.name-tag {
padding: 2em;
border-radius: 25px;
background: #f90304;
font-family: arial;
color: white;
text-align: center;
box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.75);
}
h3 {
padding: 2em 0;
background: white;
color: black;
}
p {
font-size: 24px;
font-weight: bold;
text-transform: uppercase;
}
Closing thoughts about how to create a Web Component from scratch
The game of Web Components has to be played by a handful of basic rules but when played right, the possibilities are endless! Today, we've learned step by step how to create a simple, <name-tag>
component by defining Custom Elements, passing HTML attributes, connecting the Shadow DOM, defining and cloning HTML templates, and some basic styling with CSS.
I hope this tutorial was useful and I hope to see you next time!
Top comments (1)
can all be chained,
and
append
is shorter... you don't useappendChild
s return valuesource: Web Components 102