Web Components: How to Do Them And Are They Good

Web Components: How to Do Them And Are They Good
Extremely good buttons from a NASA control panel

I've been hearing about Web Components for several years, but until recently, I hadn't taken the time to learn how they work. The MDN docs are pretty good (as usual!) but it still took me a bit of tinkering to get things working, so I figured I'd write up my notes in case an alternate explanation helped someone else get up to speed a bit faster. Below the tutorial I'll also share some opinions about where things are at right now (spoiler: Web Components are not ready for prime time yet, at least not without a framework).

Quick(ish) Web Components Tutorial

Let's say I've got this fancy looking button, defined in HTML and CSS:

<style>
  .btn {
      background:
          linear-gradient(rgb(255, 255, 255, .8), 4%, rgb(0, 0, 0, 0)),
          linear-gradient(rgb(255, 255, 255, 0) 97%, rgb(0, 0, 0, .4) 99%, rgb(120, 120, 120, 0.4)),
          /* ... */
  }

  .btn .button-well {
      width: 90%;
      height: 85%;
      /* ... */
  }

  .btn .the-button {
      height: 96%;
      width: 99%;
      border-radius: 2px;
      /* ... */
  }
</style>

<div class="btn">
  <div class="button-well">
    <div class="the-button">
      <div class="button-content">Push it</div>
    </div>
  </div>
</div>

I snipped out a lot of extra CSS in there, but you get the idea. The crucial thing about my implementation is that it uses a few different nested HTML elements. That makes it hard to reuse, because I'd have to copy and paste that same HTML structure everywhere I wanted the same button, which is fiddly and bloats up your HTML fast. If you're using a front end framework like React or a back end templating system like ERB, that's easy to solve, but how do you solve it if you want to work only with what the browser gives you?

We've had the <template> element for a while, but using it is clunky. There's no way to instantiate templates in HTML: you have to leave a placeholder where you want the element to go and then copy the template into that placeholder in JavaScript. I don't want to see <div class="a-chunky-button-will-go-here"> in my HTML, I want to see <chunky-button>.

The Whole Thing

Here's the entire component we'll be defining. I'd recommend skimming the code quickly and then reading the explanations below, then coming back up to read the whole thing more carefully. I'd also recommend opening the CodePen up in a new tab; it gets kinda squished in this blog layout.

Defining a simple component

OK, let's break all of that down. Here's all the JavaScript you need to get a basic Web Component up and running:

class ChunkyButton extends HTMLElement {
  constructor() {
    super()
  }
}

customElements.define('chunky-button', ChunkyButton)

Here's what's going on there:

  • Write a class that extends HTMLElement: this is the base class for all elements. You can also subclass other elements, both built-ins and custom.
  • Call super in the constructor, so all basic element behavior (e.g. the ability to listen to and emit events) is wired up.
  • Register your component with the global object customElements, which is a singleton instance of CustomElementRegistry.

And now I can say <chunky-button></chunky-button> in my HTML, and it's valid! Of course, it doesn't look like anything or do anything yet, but that's the boilerplate you need to start.

Your custom element name MUST contain a dash!!

To ensure there will never be a collision with current or future standard element names (which will never contain a dash), custom elements must have a dash in their name. So chunky-button is fine, but chunkybutton is not allowed.

Adding HTML to the Shadow DOM

Shadow DOM is a mechanism for isolating a tree of DOM nodes from the primary document. Let's add one to our component and put some HTML in it.

class ChunkyButton extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ open: 'true' })
    this.shadowRoot.innerHTML = `
      <div class="btn">
        <div class="button-well">
          <div class="the-button">
            <div class="button-content">Push it</div>
          </div>
        </div>
      </div>`
  }
}

If we now use <chunky-button> in a page, we'll see the text "Push it" enclosed within a few nested divs, but no styles, even if we still have that style definition from the non-component version! This is because our element's structure is isolated from the rest of the document. We'll have to define those styles inside the shadow DOM to get them working.

Add styles to the Shadow DOM

constructor() {
  /* ... */
  let style = document.createElement('style')
  style.textContent = `
    .btn {
      background: linear-gradient(rgb(255, 255, 255, .8), 4%, rgb(0, 0, 0, 0)),
     /* ... */
    }`
  this.shadowRoot.appendChild(style)

Now our button is styled. Thanks to the Shadow DOM, these styles apply only to this custom element, so anything out in the rest of the page with the class btn will not be affected by this CSS.

Add attributes

Let's make our button's text customizable via attribute. We don't have access to our element's attributes in the constructor, so we'll use one of the lifecycle callbacks:

class ChunkyButton extends HTMLElement {
/* ... */
  static get observedAttributes() {
    return ['label']
  }

  attributeChangedCallback(attr, oldValue, newValue) {
    if (attr === 'label') {
      this.shadowRoot.querySelector('button-content').innerText = newValue
    }
  }
}

Two new things here:

  • attributeChangedCallback: this runs whenever an attribute changes on our element. Crucially, the first time the attribute is processed counts as a change, so it will run as the element instantiates.
  • observedAttributes: this is a static property that lists all the attributes we care about. attributeChangedCallback will only fire if the attribute that changed is in this list.

Now we can say <chunky-button label="Nice"></chunky-button> and the contents of the button will be what we wanted!

There's a fair bit more JS and CSS than that in the CodePen above, but it's all extra stuff like wiring up click handlers, nothing to do directly with Web Components. I'd recommend opening it up in a new tab and playing around with the code to see how the components interact with the rest of the page.

That's the basics!

There's plenty more stuff we could cover, but that's enough to get up and running. As mentioned, the MDN docs are thorough and well-written, so if you want more details that's a great next stop. Some important topics we didn't cover here:

  • <slot>s for child elements
  • the other callback functions, connectedCallback, disconnectedCallback, and adoptedCallback
  • linking CSS instead of defining it inline
  • Autonomous Components (what we built here) vs Customized Built-Ins (subclassing and customizing existing HTML elements)

Opinion Zone

I'm a dependency skeptic. If I can accomplish something using only the native capabilities of a platform and a thin adapting/abstracting layer, I almost always prefer that over farming the work out to some other code that will bring its own opinions and risks into my codebase. Of course there are exceptions to this—I'm ultimately a pragmatist, if a curmudgeonly one—but I'm much slower to add libraries to a software project than the average JavaScript developer.

But let's back up. Why are Web Components exciting? In short, abstraction and encapsulation. HTML is fundamentally a document format, not a base layer for interactive applications. Of course it's possible to build great web apps, but the platform requires you to hack around this document idea to do so. Raising the level of thought from document sections to application UI components requires one of two things: a complex system of conventions (e.g. BEM CSS) and a sighing acceptance that your UI code is going to be full of <div>s, or a framework like React.

Frameworks have been giving us this ability for many years! But Web Components represent the web standards creators embracing the idea that developers really are making complex applications and shouldn't be limited to hacking around document-focused elements to do so in the platform itself. Given my dependency conservatism, you can imagine I've been eager to see them get good enough to use all by themselves right in the browser, with no frameworks or build steps or anything.

Are Web Components viable sans framework?

So are we there yet? I hate to say it, but no. As of today, writing Web Components without a tool like Stencil sucks. I hope we get there! But there are too many rough edges right now for a just-the-platform approach to be viable in a serious project.

Strings everywhere sucks!

Defining all your component CSS and HTML inside a JS template string is gnarly. You miss out on editor help like indentation and syntax highlighting, and it just looks ugly sitting there in the class definition. You can use the DOM API (document.createElement and friends) instead, and that does have some advantages, but it obfuscates structure: it's much harder to see what your element tree looks like as a flat block of JS than as a nested, indented chunk of HTML.

You can also use <template>s defined elsewhere in HTML from within a Web Component, but that's clunky and removes the easy portability factor that you get from defining everything in JS, because you can't include an HTML document in another HTML document.

Form integration sucks

Integrating custom elements with HTML forms is currently entirely ad hoc and up to you. I believe there is work being done on a new API to let custom elements sit more comfortably inside a <form>, so I'll be looking forward to that.

Customized Built-Ins suck

You can subclass an existing HTML element like <p> to add extra functionality to it. In theory, this is really cool! In practice, it's lame as heck. If I define my-cooler-paragraph by subclassing HTMLParagraphElement, I can't use it in my document by saying <my-cooler-paragraph>; I have to say <p is="my-cooler-paragraph">. That sucks! Defining my own tag was the whole reason I wanted to make a custom element to begin with!

On top of that, some elements won't allow you to attach a shadow DOM to them. If you subclass HTMLButton, you can't give it your own custom structure. This, as with my form integration complaint above, makes it cumbersome to lean on existing behavior like click handling and tab focus. It also makes accessibility much trickier to achieve than it could be, since you're throwing out much of what the browser gives you for free.

Maybe Someday

So that's Web Components. I'm excited about them, and I think one day they will be great. But right now, I wouldn't use them on a serious project without something like Stencil to bridge the usability gaps.

By the way, the buttons in my CodePen were inspired by the absolutely awesome NASA control panels of the 60s and 70s, one of which is featured in the hero image of this post. They don't make 'em like that any more!

Subscribe to Casey Brant

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe