DEV Community

Cover image for [tutorial] How to create a Web Component?
Stefan Nieuwenhuis
Stefan Nieuwenhuis

Posted on • Originally published at nhswd.com

[tutorial] How to create a Web Component?

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

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" />
    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>
Enter fullscreen mode Exit fullscreen mode

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.

  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);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>`;
  }
}
...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 this.attachShadow({mode: 'open'}); to the component's constructor (read more about Shadow DOM modes here).
  2. We also have to attach our innerHTML to the shadow root. Replace this.innerHTML with this.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>`;
}
...
Enter fullscreen mode Exit fullscreen mode

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>
`;
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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!

Discussion (1)

Collapse
dannyengelman profile image
Danny Engelman
constructor(){
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.appendChild(template.content.cloneNode(true));
}
Enter fullscreen mode Exit fullscreen mode

can all be chained,
and append is shorter... you don't use appendChilds return value

constructor(){
    super() // sets AND returns 'this'
     .attachShadow({mode: 'open'}) // sets AND returns this.shadowRoot
     .append(template.content.cloneNode(true));
}
Enter fullscreen mode Exit fullscreen mode

source: Web Components 102