Web Components 101: Tutorial - How to create a Web Component?

Web Components 101: Tutorial - How to create a Web Component?

Stefan Nieuwenhuis8 min readLast update:

Share this post

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.

What are we going to build today?

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:

  1. You can't register a Custom Element more than once.
  2. Custom Elements cannot be self-closing.
  3. 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

  1. Go to the Components IDE
  2. Click the Fork button in the top right of the screen to create your copy.
  3. 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" />
    <script src="./dist/name-tag.js" type="module"></script>  </head>

  <body>
    <h3>Hello World</h3>
    <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. I mean, what's the use of a name tag when no name is displayed?! 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.

  1. Open name-tag.js and create a class that extends the base HTMLElement class.
  2. Call super() in the class constructor. Super sets and returns the component's this scope and ensures that the right property chain is inherited.
  3. 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>.

Pro tip: super() is chainable, so we can use chaining to update the component's inner HTML directly.

This is how our components class should look like:

class UserCard extends HTMLElement {
  constructor() {
    super()
      .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>

This is how our app with the component looks like now:

Our tag name component after adding global CSS

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.

  1. Add .attachShadow({mode: 'open'}); to super() (read more about Shadow DOM modes here).
  2. We also have to attach our innerHTML to the shadow root.

Here is the diff of our constructor:

...
constructor() {
  super()
-   .innerHTML = `<h3>${this.getAttribute('name')}</h3>`;
+   .attachShadow({mode: 'open'})
+   .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()
      .attachShadow({mode: 'open'})
-     .innerHTML = `<h3>${this.getAttribute('name')}</h3>`;
+     .shadowRoot
+     .append(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.

constructor() {
  ...
  this.shadowRoot.querySelector('h3').innerText = this.getAttribute('name');
}

The result is that we see "John Doe" on our page again, 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.

Our now encapsulated tag name component with the Shadow DOM attached

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;
}

The result:

Our tag name component with the bonus CSS applied

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!