</> HTML5Advent
ENFRESDEITPT

// html · Web Platform Advent #5

Web Components: Custom Elements, Shadow DOM and templates

Build reusable HTML elements with the native Web Components APIs — define a Custom Element, encapsulate styles with the Shadow DOM, clone a <template>, and pass data with attributes.

A pile of colourful interlocking plastic building blocks of different shapes

Web Components are a set of native browser APIs that let you build your own reusable HTML elements — no framework required. A component you ship works the same in plain HTML, React, Vue or Svelte, because it is just an element the browser understands. Three APIs make it work: Custom Elements, the Shadow DOM, and the <template> tag.

Custom Elements

A Custom Element is a class extending HTMLElement, registered with a tag name that must contain a hyphen (so it can never collide with a future built-in tag):

class GreetingCard extends HTMLElement {
  connectedCallback() {
    const p = document.createElement('p');
    p.textContent = 'Hello from a custom element!';
    this.append(p);
  }
}

customElements.define('greeting-card', GreetingCard);

Once defined, you use it like any other tag:

<greeting-card></greeting-card>

The class gives you lifecycle callbacks. The most useful are connectedCallback (the element was added to the page), disconnectedCallback (it was removed), and attributeChangedCallback (a watched attribute changed).

Reacting to attributes

To pass data in, read attributes — exactly like src on an image. Declare which ones to watch with a static observedAttributes getter, then respond in attributeChangedCallback. Building the node with textContent keeps user-supplied values safe:

class GreetingCard extends HTMLElement {
  static get observedAttributes() {
    return ['name'];
  }
  attributeChangedCallback(attr, oldVal, newVal) {
    if (attr === 'name') this.render();
  }
  connectedCallback() {
    this.render();
  }
  render() {
    const name = this.getAttribute('name') || 'friend';
    this.replaceChildren();
    const p = document.createElement('p');
    p.textContent = 'Hello, ' + name + '!';
    this.append(p);
  }
}
customElements.define('greeting-card', GreetingCard);

Now <greeting-card name="Ada"></greeting-card> renders "Hello, Ada!", and changing the attribute live re-renders it.

An open metal toolbox with neatly organised tools in separate compartments
A web component is like a tool in a toolbox: a self-contained, labelled unit you reach for and reuse across many projects without rebuilding it each time.

The Shadow DOM: real encapsulation

The Shadow DOM gives an element a private subtree whose styles and markup are isolated from the rest of the page. A <style> inside it can never leak out, and outside CSS can never reach in by accident. Build it with the DOM API and a <slot> for projected content:

class FancyButton extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });

    const style = document.createElement('style');
    style.textContent =
      'button { background:#2563eb; color:#fff; border:0;' +
      ' border-radius:8px; padding:.6rem 1rem; }';

    const button = document.createElement('button');
    button.append(document.createElement('slot'));

    shadow.append(style, button);
  }
}
customElements.define('fancy-button', FancyButton);

The <slot> is a placeholder: whatever you put between the tags — <fancy-button>Save</fancy-button> — is projected into that spot, so the component wraps your content without replacing it.

Templates: clone once, reuse cheaply

The <template> element holds inert markup that is parsed but not rendered until you clone it. It is the efficient way to stamp out repeated structure:

<template id="row-tpl">
  <li><span class="label"></span></li>
</template>

<script>
  const tpl = document.getElementById('row-tpl');
  const node = tpl.content.cloneNode(true);
  node.querySelector('.label').textContent = 'Item 1';
  document.querySelector('ul').append(node);
</script>

The three pieces together

APIGives you
Custom ElementsYour own tags with lifecycle callbacks
Shadow DOMStyle and markup encapsulation, slots
<template>Reusable, cloneable inert markup

You rarely need all three at once. A simple wrapper might use only a Custom Element; a styled, self-contained widget combines a Custom Element with the Shadow DOM and a slot. Because the result is plain DOM, it drops into any stack and outlives whatever framework was popular the year you wrote it.