Through stalking the #WebComponents hashtag and my Frontend Masters course, I’m privy to a lot of developers’ first experiences with web components. There’s a wide range of people digging in, but the most common first-time experience I come across is a developer coming from a classical component framework like React with JSX going straight to writing vanilla Web Components, becoming frustrated, and then deeming web components “not ready for primetime.”

Ignoring for a moment that web components do exist in the primetime and power some big and complex primetime web applications like Adobe’s Photoshop for Web, I half-understand this perspective. I understand the desire to not have a major dependency. I hate this bloated node_modules hellhole we’ve built over the last decade and while I’m not a npm install my problems away guy… I think this puritanical approach to dependencies is a misstep when diving into web components for the first time.

The analogy I’ve been using is that this is like jumping from a tall 130 kilobyte-story building (ReactDOM) right into the zero kilobyte sewers of web components. If you take anything from this post, please understand this: web components (most likely) weren’t designed for you. Not to dissuade you from using them, but they were purposefully designed to be a low-level bare metal primitive for library authors to build on; they were designed to be used with a library, a thin layer of abstraction butter on top.

To understand this disparity further, let’s look at an example of what writing a component in a “modern” framework feels like…

// React
export default function MyApp() {
	handleClick() {
		alert('hi')
	}
	
  return (
    <div>
      <h1>Welcome to my app</h1>
      <button onClick={handleClick}>Im a button</button>
    </div>
  );
}

Can you do this in vanilla web components? Sure. But it looks like this…

// Vanilla web component
const myAppTmpl = document.createElement('template')
myAppTmpl.innerHTML = `
  <h1>Welcome to my app</h1>
  <button>I’m a button</button>
`;

class MyApp extends HTMLElement {
	constructor() {
		super();
		this._shadowRoot = this.attachShadow({ mode: 'open' })
		this._shadowRoot.appendChild(myAppTmpl.content.cloneNode(true))
		this._shadowRoot
      .querySelector('button')
      .addEventListener('click', this.handleClick);	
	}
	
	handleClick() {
		alert('hi')
	}
}

customElements.define('my-app', MyApp)

This is a boring, imperative set of instructions for building the component with a little bit of dangerouslySetInnerHTML1 mixed in there… and ugchk… it sucks. It’s even more verbose with more interactive elements or a slew of reactive props and attributes.

Let’s see what 7 kilobytes of Lit gets us….

import { LitElement, html } from 'lit'

class MyApp extends LitElement {
	handleClick() {
		alert('hi')
	}
	
  render() {
		return html`
      <h1>Welcome to my app</h1>
      <button @click=${this.handleClick}>I’m a button</button>
    `;
  }
}

customElements.define('my-app', MyApp)

Now we have a component that’s almost identical to the familiar world of JSX but without any Babel transforms or build steps. For 7 kilobytes you get a lot more than some syntactic sugar, you get…

  • A thin, tree-shakeable layer of abstraction for a modern developer experience
  • Reactive updates without VDOM
  • A more granular component lifecycle with a render() function
  • A cleaner way of registering and using reactive attributes and properties without overloading the attributeChangedCallback
  • Template directives like @click for event handling and directives for JavaScript values (arrays, objects, etc).
  • And an html tagged template literal with an under-appreciated superpower ✨ that gives your components atomic updates under-the-hood.

There’s value in learning how bare metal vanilla web components work in the same way there’s value in knowing how Intl.RelativeTimeFormat() works, but you probably want to use Day.js for your day-to-day work. You can totally write your own base class abstraction – and I want you to have the JeffElement base class of your dreams, I do – but you may find out (like Cory LaViska from Shoelace found out) that after you write all your little helper functions and utilities that you’ll end up with something almost the exact same size and feature set as Lit, but not as well supported nor as battle-tested.

This makes me sound anti-vanilla web components and I’m not that by any means. Vanilla web components are a perfect fit for standalone components and the Light DOM-forward flavor of “HTML web components”, but I think the people having the most fun in this space are JavaScript minimalists who already prefer writing vanilla JavaScript. People like myself.

“If I have to use a library how is this any different than any other framework lock-in?” This is a valid question and one worthy of its own post, but I think you’ll find the lock-in costs of a web component library pretty minimal. Because all web components libraries extend a common base class, there’s a linear pathway out of vendor lock-in if necessary.

What I’m saying is this; next time you’re thinking about jumping from 130 kilobytes of developer convenience, maybe consider giving yourself a 7 kilobyte landing pad to cushion the fall.

  1. There’s ways around the innerHTML call like by writing the template in your HTML instead.