In this tutorial, we will explore Custom Elements, a feature in Web Components, which allows us to create new HTML tags. Initially, I didn’t see the value in Custom Elements until I used the CSS Doodle library, which uses Custom Elements to create stunning CSS animations. This experience sparked my curiosity, leading me to dive deeper into the world of Web Components.

Before we proceed, please note that this tutorial covers Custom Elements version 1, the latest release of the standard at the time of writing.

Custom Elements are not meant to replace popular frameworks like React, Angular or Vue. Instead, they introduce a whole new concept. By accessing the customElements property on the window global object, we can utilize the CustomElementRegistry object, which provides several methods for registering and querying Custom Elements.

To create a custom element, we first need to define a new HTML tag by creating a class that extends the built-in HTMLElement class:

class CustomTitle extends HTMLElement {
 //...
}

Inside the class constructor, we can associate custom CSS, JavaScript, and HTML with our new tag using the Shadow DOM. This allows us to encapsulate a lot of functionality within the custom element, while only displaying the tag itself in the HTML. We start by initializing the constructor and calling the attachShadow() method of the HTMLElement:

class CustomTitle extends HTMLElement {
 constructor() {
  super();
  this.attachShadow({ mode: 'open' });
  //...
 }
}

We can then set the innerHTML of the shadow root to define the content of the custom element:

class CustomTitle extends HTMLElement {
 constructor() {
  super();
  this.attachShadow({ mode: 'open' });
  this.shadowRoot.innerHTML = `
   <h1>My Custom Title!</h1>
  `;
 }
}

Note: You can add as many tags as you want within the innerHTML property.

After defining the custom element class, we can add it to the window.customElements:

window.customElements.define('custom-title', CustomTitle);

Now, we can use the newly defined <custom-title></custom-title> custom element in our HTML page.

Keep in mind that self-closing tags (e.g., <custom-title />) are not allowed according to the standard. Also, notice that a dash - is required in the tag name for custom elements. This distinguishes them from built-in tags.

Once we have the custom element on the page, we can apply CSS and JavaScript targeting to it, just like we do with other HTML elements.

To provide custom CSS for the element, you can include a style tag within the constructor:

class CustomTitle extends HTMLElement {
 constructor() {
  super();
  this.attachShadow({ mode: 'open' });
  this.shadowRoot.innerHTML = `
   <style>
    h1 {
     font-size: 7rem;
     color: #000;
     font-family: Helvetica;
     text-align: center;
    }
   </style>
   <h1>My Custom Title!</h1>
  `;
 }
}

Alternatively, you can embed JavaScript within the constructor by defining event listeners or other functionalities.

There is also a shorthand syntax if you prefer to define the custom element class inline:

window.customElements.define('custom-title', class extends HTMLElement {
 constructor() {
  //...
 }
});

Another approach is to use a template tag in HTML, defining the HTML and CSS content there, and then referencing it within the custom element constructor:

<template id="custom-title-template">
 <style>
  h1 {
   font-size: 7rem;
   color: #000;
   font-family: Helvetica;
   text-align: center;
  }
 </style>
 <h1>My Custom Title!</h1>
</template>

<custom-title></custom-title>
class CustomTitle extends HTMLElement {
 constructor() {
  super();
  this.attachShadow({ mode: 'open' });
  const tmpl = document.querySelector('#custom-title-template');
  this.shadowRoot.appendChild(tmpl.content.cloneNode(true));
 }
}

window.customElements.define('custom-title', CustomTitle);

Aside from the constructor, Custom Element classes can define additional methods known as lifecycle hooks. These methods are executed at specific times during the element lifecycle, including when the element is inserted into or removed from the DOM, when an observed attribute changes, or when the element is adopted by a new document. The available lifecycle hooks are connectedCallback, disconnectedCallback, attributeChangedCallback, and adoptedCallback.

For example, the attributeChangedCallback() method is invoked when an observed attribute changes, and it receives three parameters: the attribute name, the old value, and the new value. To observe attributes, we need to define them in the observedAttributes static method, which returns an array of observed attributes:

class CustomTitle extends HTMLElement {
 constructor() {
  //...
 }

 static get observedAttributes() {
  return ['disabled'];
 }

 attributeChangedCallback(attrName, oldVal, newVal) {
  //...
 }
}

We can also define custom attributes for our custom elements by adding getter and setter methods:

class CustomTitle extends HTMLElement {
 static get observedAttributes() {
  return ['mycoolattribute'];
 }

 get mycoolattribute() {
  return this.getAttribute('mycoolattribute');
 }

 set mycoolattribute(value) {
  this.setAttribute('mycoolattribute', value);
 }
}

To style a custom element that might not be defined immediately, such as when JavaScript takes some time to load, we can use a CSS pseudo class :not(:defined) to set initial height and fade-in effects:

custom-title:not(:defined) {
 display: block;
 height: 400px;
 opacity: 0;
 transition: opacity 0.5s ease-in-out;
}

It is worth noting that while current versions of Firefox, Safari, and Chrome support Custom Elements, Internet Explorer does not, and Edge has limited support. The polyfill can be used to add better support for older browsers.

In conclusion, Custom Elements in Web Components provide a powerful way to create and use our own HTML tags, defining their associated CSS and JavaScript. While they are not a replacement for popular frameworks, they offer a fresh perspective on web development and empower developers to build reusable and encapsulated components.