Dec 19, 2022

Step into the light (DOM)

For a long time I didn’t know what to do with web components. There wasn’t a lot of writing around them outside of the spec, all the stuff around shadow DOM and templates made it feel inaccessible, and I just didn’t see a compelling reason to bother with any of it (something something PWAs).

That changed the day @javan clued me in to the fact that it is totally OK, and often ideal, to just use the custom element part of the spec. No shadow DOM, no templates, just the regular old DOM, which we now get to call the much cooler sounding “light DOM”.

Why would you want to do such a thing?

  1. Custom elements have the very cool ability manage themselves via connectedCallback and disconnectedCallback. These are the unsung heroes of the spec; the things that make web components the can-do workhorses that they are. The second you step out of a strictly MPA scenario, these are very, very handy.
  2. Progressive enhancement is straightforward. Any markup you add inside your custom element is just… there, javascript or no. Wrap standard interactive elements like details, textarea, input, etc in custom elements that enhances them, and you’ve got yourself a solid progressive enhancement story.
  3. Custom elements enforce good javascript patterns. This one took me a while to spot, but it’s the thing that’s made custom elements the default way I write vanilla javascript these days (take a peek under the hood for examples). Vanilla javascript always felt spaghetti-ish in my clumsy hands, but there’s only one way to write custom elements. There’s no making a custom element without writing it as a class. For me, it’s meant an overall improvement in code quality.

Here’s an example of a progressively enhanced details element called small-details that makes a details element expand into what looks like an unordered list with header once the viewport is wide enough:

/* ----------------------------------------------------------------------------
details element that defaults to closed @small and open @medium+
---------------------------------------------------------------------------- */

export default class SmallDetails extends HTMLElement {
  listen() {
    this.mediaQuery.addEventListener('change', (e) => {
      this.update(e.matches);
    });
  }

  update(matches) {
    if (matches) {
      this.details.open = false;
      this.summary.tabIndex = 0;
    } else {
      this.details.open = true;
      this.summary.tabIndex = -1;
    }
  }

  connectedCallback() {
    this.details = this.querySelector('details');
    this.summary = this.querySelector('summary');
    this.mediaQuery = window.matchMedia('(max-width: 49.999em)');
    this.update(this.mediaQuery.matches);
    this.listen();
  }
}

window.customElements.define('small-details', SmallDetails);

And here’s how it gets used:

<small-details>
  <details>
    <summary>Only interactive @small</summary>
    <ul>
      <li><a href="/link-to-page/">a page</a></li>
      <li><a href="/another-page/">another page</a></li>
    </ul>
  </details>
</small-details>

Nothing fancy or complicated. Just clear, simple, and durable code, and a great progressive enhancement story. Custom elements are good.

Read more notes