A common web component learning blunder
28 May 2024 | 2:17 pm

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.


Dave Goes Microsoft
16 May 2024 | 2:02 pm

Last Monday was my first day as an official employee of Microsoft where I’ll be working on web components as part of the Fluent design system team. As longtime readers already know, I’ve had a long term relationship with Microsoft – from Paravel’s 2012 responsive redesign of the Microsoft homepage to the five year #davegoeswindows stunt –  it feels like a new chapter in the career story arc to finally acquire one of the famous blue badges. I’m still new and have barely setup my computer but so far my team of peers, the larger group, the project itself, and the other folks across Microsoft I’ve connected with are all great.

Going from a company with two coworkers to a company with 200K coworkers is certainly an adjustment. It’s my first job in 18 years where I’m not working for myself but by far the biggest eye-opener throughout this process was doing tech interviews! I learned a lot about myself; like how after decades of coding in a room by myself, performing in front of someone else isn’t natural for me. Weirdly for me, a live demo in front of thousands of people… no problem. A random generated coding challenge in front of one person… palms sweaty, mom’s spaghetti levels of difficult. I also learned that too much caffeine and the panic-flavored adrenaline of interviewing is a lot of chemistry for my active brain to process.

I eventually figured out how to interview and I had a lot of great conversations with great people at great companies. That said, this experience left me with lingering qualms about the tech interview process. A lot of it comes down to the information asymmetry where the seller (the hiring company) has more information than the buyer (the job candidate) and it’s hard to get any feedback for self-improvement. Even in my limited experience, it’s not uncommon to sink 15+ hours into a take home coding test and interview loop only to receive a terse rejection. Granted there’s promise of a six figure salary at the end of the rainbow, these jobs don’t fall out of the sky so you need to put in work, but I think that situation needs to be a bit more equitable to candidates – a Newtonian dynamic of matching effort.

One question they ask you at interviews is “What are you looking for in your next role?” and while that sparks thousands of ideas, I boiled my needs and wants down to two core concepts:

  • Be a part of a larger team of engineers - I’d like to work on a larger team of developers. I want to be in a situation where I can actively and passively learn from other engineers who are subject matter experts in different subjects. As a life-long learner, I’d like to take myself out of the “lone developer” paradigm and absorb as much as I can.
  • Be tangential to the money machine - When you run your own business there’s a tight coupling between how much you work and how much money you make and you’re constantly aware of that fact. After 18 years of running my own business and two particularly intense years of startup burnout, I’d like to try something different and play a more supportive operational role for a bit.

I think I found that in Microsoft. There’s a multitude of people I can ping about niche technology choices. There’s even access to a library of research papers. And already I can see how operating in a product support role seems to provide more opportunity for strategy to the broader needs of the organization as opposed to reactivity to the needs du jour that happen in Productland.

I’m sure throughput will be a bit slower without direct access to the publish to production button. I’m sure there’s topics I won’t be able to talk about on this here blog (but I tend not to blog about specific work-related activities here anyways so that won’t change). And I’m sure I’ll have to put a disclaimer here and there that these ideas are my own and not reflective of my employer. Henceforth and furthermore all bad ideas are copyright of Dave Rupert LLC®.

It’s the end of an era for sure but also the beginning of a new one and potentially the beginning of lots of new ones, who knows. Thanks to Trent and Reagan. Thanks to everyone who provided emotional support on this journey. Thanks to esteemed friends who provided referrals. Given the current macroeconomic situation, I feel lucky to have landed somewhere familiar with great opportunities and many Dave Rupert-shaped problems.


A quick light-dark() experiment
5 May 2024 | 3:27 pm

I wanted to experiment with the new CSS function light-dark() and get a sense of how to use it in a CSS architecture of nested (web) components. I think it’s going to be a powerful tool in the new responsive world of component architecture but I don’t want to recommend something unless I have experience with it in a project first.

My first pass was to add light-dark() to my components…

/* global.css */
:root {
  --dark: #000;
  --light: #fff;
}

/* Inside <my-component>'s Shadow DOM */
:host {
  background-color: light-dark(var(--light), var(--dark));
  color: light-dark(var(--dark), var(--light));
}

But if every component is in charge of it’s own light-dark() handling for border, background, and color on every element… the codebase will get messy managing dark mode in a lot of different places, leading to a lot of inconsistencies over time. A more elegant solution for me would be to handle this job in a single location at the root scope level and leverage the cascade a bit.

:root {
  color-scheme:  light dark;
  --surface-color: light-dark( #fff, #000 );
  --text-color: light-dark( #000, #fff );
}

The nice thing about using light-dark() at the root token level is your components can be dumber. You provide default light-dark experience and, like good children, your components abide in their parent’s decision. Of course, due to the nature of CSS custom properties, your components aren’t locked into the system and your component level styles can opt out (read: not include) or override the global variables if necessary.

/* Inside <my-component>'s Shadow DOM */
:host {
  background: var(--surface-color);
  color: var(--text-color);
}
/* this is a real example from my past */
:host[theme="lunar-new-year"] {
  --surface-color: red;
  --text-color: black;
}

At this point in the experiment I was pleased with the results… until I deployed it to production. I overestimated the browser support for light-dark() and it didn’t work on my phone running Safari 17.4 (but it’s coming in Safari 17.5). I replicated the issue by changing light-dark() to light-d0rk() to verify and fixed it by adding a tried-and-true CSS @supports query.

:root {
  --surface-color: #000;
  --text-color: #fff;

	/* NOTE: For Safari 17.4 (2024-05) */
  @supports (color: light-dark(black, white)) {
    color-scheme:  light dark;
	  --surface-color: light-dark( #fff, #000 );
	  --text-color: light-dark( #000, #fff );
  }
}

Now Safari 17.4 and other browsers that lack support will only have a dark theme and newer browsers will get the light-dark() enhancement. I also threw a little NOTE: and datestamp in there so future versions of me will know when and why I built this fence.



More News from this Feed See Full Web Site