Web components: custom elements
Tags: webcomponents, javascript, css
Published: 2022-11-30 00:30:38

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.

```js
class MyButton extends HTMLElement {
  // (...)
}
```

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

```js
class MyButton extends HTMLElement {
  attributeChangedCallback(name, oldValue, newValue) {
    // (...)
  }
}
```

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

```html
<my-button variant="primary">Click me</my-button>
```



## Iteration 1: The simplest custom element

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

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

  <body>
    <my-button></my-button>
  </body>
</html>
```

```js
// my-button.js
class MyWebComponent extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<button>hola</button>`;
  }
}

customElements.define('my-button', MyWebComponent);
```

{% embed https://stackblitz.com/edit/web-platform-pr48fu?embed=1&file=index.html&view=preview %}

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.

```html
<!-- 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>
```

```js
// 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);
```

{% embed https://stackblitz.com/edit/web-platform-bfbpzh?embed=1&view=preview %}

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**.