DEV Community

Cover image for A Quick Guide to Custom HTML Elements
Deon Rich
Deon Rich

Posted on

A Quick Guide to Custom HTML Elements

After getting to know the component based system of modern frameworks such as React and Angular, i was immediately interested when i came across the term "Web Components". After taking a deep dive into the subject, i thought it would be worth sharing. So, today im going to provide a quick guide to Web Components, how they operate, and how we can create our own custom HTML elements by applying this concept!

What are Web Components?

A Web Component serves as a label for any HTML Element that possesses its own underlying DOM tree, CSS styles, and even scripts, that are separate from the reset of the DOM and encapsulated completely by that element. This underlying DOM tree is refered to as the Shadow DOM.

The Shadow DOM

To further understand the Shadow DOM, take the contents of the following HTML <input> as an example:

shadow DOM of an input element of type "range"

Inside of the input the first thing we see is #shadow-root. This simply represents the root of the <input> element's shadow DOM, much like how <html> is the root element of the document. The element containing the #shadow-root itself is refered to as the "shadow host", which in this example is the <input>. Everything that comes after #shadow-root is the contents of the element's shadow DOM. Simple, right? :)

Not all, but many elements are capable of having a Shadow DOM attached to them, which can serve useful if you require a quick way to provide encapsulation and abstraction in your code.

In this particular case, the Shadow DOM of the input element contains only two <div> elements. As you may have been able to tell by the ids of each element, these are used to create the track, and thumb peices of the range slider.

This is a perfect example of the Shadow DOM in action. It allows things like <video> and <audio> elements to hide their moving parts and functionality away from the rest of the document, simplifying and keeping the program organized.

We can take this a step further, by implementing the Shadow DOM API to create our own custom HTML components..😁

Creating our first Custom HTML Element

Before starting to build our custom element, we first need to understand some criteria it has to meet to be considered one:

  1. It has to have a constructor (usually via a class)
  2. It needs to posses a Shadow DOM
  3. It must be registered within the CustomElementRegistry
  4. It can optionally use a <template>

If any of these steps dont immediately make sence, dont sweat it, everything will become clear as i walk you through it.

For demonstration in this short tutorial ill be making a custom HTML element named <type-writer>. The final product will look like this:

my custom "typewriter element"

I decided to make a simple custom element which consists of a <textarea>, and a some <button> elements to serve as keys to show text onto the screen.

An easy example that should clearly demonstrate how we can create somewhat complex custom elements using this API.

Without further ado, lets start with the first step..👇

Creating a Web Component Constructor

Before we do anything else, its mandatory that we create a constructor function for our custom element. Its purpose is to initiate our component and attach any functionality it might have to it, and will be called each time a new instance of our web component is created.

Below i create the constructor for our <typewriter> element using a class:

// Extend generic HTMLElement interface
class Typewriter extends HTMLElement {
 constructor() {
  super();
  // implement functionality...
 }
}
Enter fullscreen mode Exit fullscreen mode

I've named the class Typewriter, though this dosent serve as the name we'll use to write it into our HTML (<type-writer>), so you can call it whatever you want. Ill show how you can define a tagname for it in a later section.

When creating your constructor, its required that you extend the functionality of an existing built-in HTML element, or the generic HTMLElement interface. This is so that your custom element inherits all of the same required properties that all built-in elements do. Otherwise your custom element wouldn't be compatible with the DOM.

There are two types of web components that can be created, based on the interface that you're extending:

  • Customizable components: Custom elements where the constructor of which, extends upon the functionality and properties of an already existing built in element.

Its constructor would be similar to -

// Extend functionality of a <div> element
class Superdiv extends HTMLDivElement {
constructor() {
 super();
}
}
Enter fullscreen mode Exit fullscreen mode

And would be shown in HTML as -

<div is="super-div"></div>
Enter fullscreen mode Exit fullscreen mode
  • Autonomous components: Custom elements where the constructor of which, extends the functionality of the generic HTMLElement interface. These elements apply their own functionality, and share no properties in common with other built-in HTML elements other than those defined in the HTMLElement interface (which serves as the bare-bones, or, minimum required properties for every HTML element).

Its constructor would be similar to -

// Extend the generic HTMLElement interface
class MyElement extends HTMLElement {
 constructor() {
  super();
 }
}
Enter fullscreen mode Exit fullscreen mode

And would be shown in HTML as -

<my-element></my-element>
Enter fullscreen mode Exit fullscreen mode

In this case, our <type-writer> element is an autonomous component, because it extends the HTMLElement interface. I decided on an autonomous component because i didn't find it nesessary to extend another elements functionality, but mainly because i find writing <type-writer> into HTML rather than somthing like <div is="type-writer"></div> much more attractive..👌😎

Attaching a Shadow DOM

Now that we've got a container for our elements functionality, we need to attach a Shadow DOM onto our element upon its initiation.

// Extend generic HTMLElement interface
class Typewriter extends HTMLElement {
 constructor() {
  super();
  // attach shadow DOM to element
   let shadow = this.attachShadow({mode: "closed"});
  // implement functionality...
 }
}
Enter fullscreen mode Exit fullscreen mode

After calling super, i call the attachShadow method of our new element (which was inherited from HTMLElement) which returns the newly created #shadow-root, which i store in the variable shadow.

The one parameter it takes in is an object which contains a couple of configuration options. The mode property indicates weather or not the elements within the #shadow-root of our element are accessable outside of the shadow host. Ive set it to "closed" so that they aren't accessable, but you can use "open" as well depending on your programs reqirements.

We now have a reference to our shadow root, so we can go ahead and start appending content to it to construct our element!

Filling Our Shadow DOM

I would say theres two good ways to go about adding content to the Shadow DOM once its attached; you can create elements and append them to the #shadow-root via normal DOM methods, or you could utilize a <template>.

The <template> Element

The HTML template element is a unique element, used to hold content which will later be implemented.

<!-- a simple template example -->
<body>
 <template id="my-template">
  <!-- template content -->
  <p>A simple template!</p>
 </template>
</body>
Enter fullscreen mode Exit fullscreen mode

<template> elements are parsed in HTML, but not rendered. Each <template> will have its own content property, which is DocumentFragment(much like a React fragment) of its contents. We can then clone this content and append it to our elements #shadow-root.

The <template> element can also be used in conjunction with the <slot> element, which serves as a placeholder for you to add dynamic content into a template. Its a tad out of the scope of this guide, but you can read more about it here.

Below i create a template containing the content that will be inside of the #shadow-root of my <type-writer> element, and append it:

  <template id="typewriter-template">
    <style>
    /* applying default styles to our element */
      textarea {
        background: black;
        color: limegreen;
        width: 200px;
        height: 70px;
        box-sizing: border-box;
        border: none;
        padding: 0.5em;
      }

      div {
        width: 200px;
        display: grid;
        height: 200px;
        grid-template-columns: repeat(4, auto);
      }

      span {
        height: 270px;
        width: 200px;
        display: grid;
        border-radius: 10px;
        overflow: hidden;
      }
    </style>
    <span> 
    <!-- screen -->
      <textarea readonly placeholder="..."></textarea>
    <!-- button container -->
      <div></div>
    </span>
  </template>
Enter fullscreen mode Exit fullscreen mode
class Typewriter extends HTMLElement {
 constructor() {
  super();
  // attach shadow DOM to element
   let shadow = this.attachShadow({mode: "closed"});
  // Apply template
   let template = document.getElementById("typewriter-template");
shadow.appendChild(template.content.cloneNode(true));
  // implement functionality...
 }
}
Enter fullscreen mode Exit fullscreen mode

Lastly, before implementing the final step, i'll add in all of the functionality for my custom <type-writer> element, completing our constructor:

class Typewriter extends HTMLElement {
  constructor() {
    super();

// attach shadow DOM
    let shadow = this.attachShadow({ mode: "closed" }),
      template = document.getElementById("typewriter-template");

// implement template
shadow.appendChild(template.content.cloneNode(true));

// Adding keys and additional functions
    let keys = shadow.querySelector("div");
    let screen = shadow.querySelector("textarea");
    let typed = new Event("typed");
    screen.addEventListener("typed", () => {
      screen.innerHTML = screen.innerHTML + "|";
    });
    for (let i = 97; i <= 122; i++) {
      let key = document.createElement("button");
      key.addEventListener("click", (e) => {
        backspc();
        screen.innerHTML = screen.innerHTML + e.target.innerText;
        screen.dispatchEvent(typed);
      });
      key.innerText = String.fromCharCode(i);
      keys.appendChild(key);
    }
    let del = document.createElement("button"),
      spc = document.createElement("button");
    del.innerText = "DEL";
    function backspc() {
      let l = screen.innerHTML.split("");
      l.pop();
      console.log(l);
      screen.innerHTML = l.join("");
    }
    del.addEventListener("click", () => {
      backspc();
      backspc();
      screen.dispatchEvent(typed);
    });
    keys.appendChild(del);

    spc.innerText = "SPC";
    spc.addEventListener("click", () => {
      backspc();
      screen.innerHTML = screen.innerHTML + " ";
      screen.dispatchEvent(typed);
    });
    keys.appendChild(spc);
  }
}
Enter fullscreen mode Exit fullscreen mode

Registering our <type-writer> Element

Before we can use our new <type-writer> tag in our HTML code, we lastly need to register our component within the CustomElementRegistry. The CustomElementRegistry interface is implemented by the customElements object, which is where constructors for custom elements are stored, and can be accessed.

We can register our new element by using the customElements.define() method:

customElements.define("type-writer",Typewriter);
Enter fullscreen mode Exit fullscreen mode

The first parameter is the tagname we want for our new element. This can be anything as long as a dash (-) is included within it. And then our second parameter is simply the constructor thats associated with our new custom element.

Once thats done, you can use it in your HTML and refer to it in your CSS and Javascript just as you would with any other built-in element! Pretty cool, huh?

<type-writer></type-writer>
Enter fullscreen mode Exit fullscreen mode

Conclusion

And there you have it! I hoped this guide served useful for understanding how Web Components work, and how we can use the Shadow DOM and Custom elements APIs to create our own HTML elements.

Good luck, and happy coding! 😁

Discussion (4)

Collapse
zippcodder profile image
Deon Rich Author

Hello everyone, this is my first post here on DEV, so any advice, insight or criticisms will be appreciated!

Collapse
dannyengelman profile image
Danny Engelman

Read the dev.to/p/editor_guide
Especially the part where they included running Code snippets; I prefer JSFiddle because you can tell JSFidlle what to show, The Result, html, css or JS

Collapse
dannyengelman profile image
Danny Engelman

Read what is already here on Dev.To tagged '#webcomponents'.
Helps you write some better code: dev.to/dannyengelman/web-component...

Collapse
zippcodder profile image
Deon Rich Author

Both references were extreamly helpful, thanks for the feedback!