Dark Mode

Nick Van Exan

Software Developer

Web Components

They keep getting better every day

A while back I re-wrote my website. Unhappy with the performance hits of large frameworks, I ripped out Next.JS and React along with it. I replaced those with my own custom build system powered by Markdoc. You can read more about the journey and the performance gains here.

At the time, I made a decision to use an HTML renderer for my pages and to use Web Components to add progressive enhancement where interactivity was desired.

Web Components were a mixed bag of a choice. They run natively in the browsers, and so no framework is required to create custom elements and leverage the Shadow DOM. But their API is not ergonomic (i.e. they're super verbose to write). And they necessarily require a browser to run, which for a time ruled out server-side rendering (SSR).

But things are changing. Quickly. And I think this is really good news.

Ergonomics

There are a number of non-framework... err... frameworks... emerging for creating Web Components, which improve the ergonomics of writing custom elements. I'll briefly mention my favourites.

Lit is probably the most popular and well known. You can, for example, start a new Vite project with Lit. It is also a default in the Open WC generators. Lit is cool because it allows you to write your Web Components in TypeScript, with JSX-like syntax. It uses tagged template literals, which means you don't need to learn custom syntax and can start writing custom elements without fuss. In addition, Lit has a tiny footprint, weighing in at 5kb (minified and compressed). A small overhead for a dramatically better authoring experience.

Here's an example of a web component written in Lit:

import {html, css, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
  static styles = css`p { color: blue }`;

  @property()
  name = 'Somebody';

  render() {
    return html`<p>Hello, ${this.name}!</p>`;
  }
}

Stencil is a more comprehensive library than Lit for building reusable, scalable design systems. On the surface, Stencil seems similar to Lit. You can use TypeScript. The syntax is JSX-like. There are decorators that shortcut a lot of things for you. But the true power of Stencil is, for me, in its tooling as a library for creating design systems. Out of the box, it gives you everything you need to write your components, test them, document them, and define their types.

Here's an example of a web component written in Stencil:

@Component({
  tag: 'ds-text',
  styleUrl: 'ds-text.css',
  shadow: true,
})
export class Text {
  render() {
    return (
      <slot name=ds-corp-text>
        Your Text Rendered Here
      </slot>
    );
  }
}

But wait, there's more!

Stencil doesn't just allow you to build and output Web Components for use in your projects; it also can compile your Web Component into framework-specific component code (such as Vue, React, Angular, etc.). This makes Stencil a great choice for teams looking to build a future proof design system at organizations that might be using Micro Frontends with different frameworks and tech stacks.1 You can write a component once, and compile it for Web (as a custom element), React, Vue, etc.

SSR and Declarative Shadow Dom

Modern web sites and applications rely on static-site generation (SSG) and server-side rendering (SSR) to dramatically improve performance outcomes over traditional client-side only rendered (CSR) applications.

Using Web Components in your architecture can create a lot of headache if you're doing SSR. Why? Because until recently, the only way to use the native Shadow DOM was to construct a shadowRoot using JavaScript at runtime. There was no built-in way to express Shadow Roots in the server-generated HTML.

An important negative side effect of this lack of SSR support is layout shifting after the page has loaded, or temporary displays of a flash of unstyled content ("FOUC") while loading the Shadow Root's stylesheets. When a custom element is rendered by the server, but then has its shadowRoot created at runtime to "hydrate" the component, this can be the result. And it requires clever hacks to engineer around.

Until now.

Declarative Shadow DOM (DSD) is a web platform feature currently in the standardization process that removes this limitation, bringing Shadow DOM to the server. And last week, the Chrome Dev team announced it would be enabled by default in Chrome 111.

You can read more about how DSD works at the links above. But the TLDR is, with the introduction of DSD, it's now possible for a Custom Element to have a shadowRoot before it gets upgraded. This means the element will have a shadowRoot property already available when it is instantiated, without your code having to explicitly create one.

As a new web platform API, Declarative Shadow DOM does not yet have widespread support across all browsers. But change is in the air. And the future for Web Components is looking bright.

  1. Indeed, some of the issues I mentioned in this earlier post re disparate MFE tech stacks could fall away somewhat, at least at a technical level, if you use Stencil for your design system. Organizationally, I'd still recommend keeping a unifying stack for your MFEs.

Nice, you made it to the end of this post. Thanks for reading. Other posts by me are over on my writing page. If you'd like to subscribe to this blog, you can do so via RSS or Email Newsletter. If you found this post helpful, and want to support more of this content, you can also buy me a coffee.