DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Web components: custom elements
Joan Llenas MasΓ³
Joan Llenas MasΓ³

Posted on

Web components: custom elements

The web Components v1 specification consists of three main technologies that can be used to create reusable custom elements:

  • Custom elements
  • HTML templates
  • Shadow DOM

Custom elements

Custom elements allow you to define new tags by declaring classes that extend the HTMLElement base class.

class MyButton extends HTMLElement {
  // (...)
}
Enter fullscreen mode Exit fullscreen mode

The HTMLElement base class exposes callback functions you can use to do stuff at any point in the component lifecycle.

class MyButton extends HTMLElement {
  attributeChangedCallback(name, oldValue, newValue) {
    // (...)
  }
}
Enter fullscreen mode Exit fullscreen mode

With custom elements, you define the component API by exposing attributes and events.

<my-button variant="primary">Click me</my-button>
Enter fullscreen mode Exit fullscreen mode

Iteration 1: The simplest custom element

Let's create the simplest custom element ever so we understand the basics.

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <script src="my-button.js"></script>
  </head>

  <body>
    <my-button></my-button>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode
// my-button.js
class MyWebComponent extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<button>hola</button>`;
  }
}

customElements.define('my-button', MyWebComponent);
Enter fullscreen mode Exit fullscreen mode

A couple of key parts in this example:

  • The MyWebComponent class uses the connectedCallback life cycle method to add the <button> markup. Note that we are using this.innerHTML = ..., that is because we are inside the HTMLElement! We have access to all DOM APIs, which will be scoped to the element.
  • The window.customElements gives you access to the custom elements registry, and the define() method registers the declared class with the provided tag name (which, by the way, must have a dash in its name to ensure there are no collisions with built-in HTML tags).

In this contrived example, we only focused on creating the custom element, which leaves a lot to be desired! Let's spice things up a bit!

Iteration 2: Declarative component API with attributes

Let's expose the button label as an attribute and allow setting the button look and feel based on the variant attribute value.

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <script src="my-button.js"></script>
    <style>
      body {
        display: flex;
        flex-direction: column;
        gap: 8px;
      }
      button {
        cursor: pointer;
        font-size: 20px;
        font-weight: 700;
        padding: 12px;
        min-width: 180px;
        border-radius: 12px;
      }
      button.primary {
        background-color: #0b66fa;
        color: #fff;
        border: 0;
      }
      button.secondary {
        border: 1px solid rgba(0, 0, 0, 0.12);
        background-color: #fff;
        color: #000000de;
      }
    </style>
  </head>

  <body>
    <my-button label="Default"></my-button>
    <my-button variant="primary" label="Primary"></my-button>
    <my-button variant="secondary" label="Secondary"></my-button>
    <button class="primary">Plain HTML &lt;button&gt;</button>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode
// my-button.js
class MyWebComponent extends HTMLElement {
  static get observedAttributes() {
    return ['variant', 'label'];
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback() {
    this.render();
  }

  render() {
    const label = this.getAttribute('label') || '';
    const variant = this.getAttribute('variant') || '';
    this.innerHTML = `
      <button class="${variant}">${label}</button>
    `;
  }
}

customElements.define('my-button', MyWebComponent);
Enter fullscreen mode Exit fullscreen mode

A couple of interesting facts about this example:

  • If we want the component to be notified of changes to its properties via the attributeChangedCallback() method, we have to declare them using the observedAttributes() static method.
  • We've defined a few global CSS styles, and as we can see, all buttons are being styled, whether inside a custom element or not. That's not what we want for our web components. But fear not! Keep reading and find out how to solve this issue.

Coming up next

In the next article we'll isolate our component from the rest of the page while introducing the Shadow DOM, templates and slots.

Top comments (0)

Join us at DEV
Yes, this is technically an β€œad”, but really we just want to ask if you want to join DEV. We have 900k+ developers reading, posting, and enjoying community, and would love to have you. Β  Create an account and continue your coding journey.