Web Components for Framework Developers

Web Components are great for building Design Systems that need to work across multiple frameworks. If you've been working in framework-land for quite some time now, you may find some topics confusing at first. In this post, I share some of the learnings I've experienced over the past year working with Web Components.

Learn "The Platform™"

To summarize this entire post, I'd say: learn or relearn how to use JavaScript, CSS, and HTML without a framework.

A lot of us started learning web development this way; however, we quickly switched to a framework because that's what the industry forced us to do. We've forgotten a lot of the fundamentals. Go build a little web app with no framework and get back to the basics of web development. Because Web Components are part of the native Platform and treated as first-class citizens by the browser, remembering how to add event listeners and attributes goes a long way when diving into Web Components.

For those that don't have the time to go do this (I get it!), here's a quick brain dump from me.

Attributes versus Properties

The first thing you should do is go read Jake Archibald's excellent post on HTML attributes vs DOM properties then come back here. Seriously, I'll wait. It'll take you like 5 minutes. Go read it!

Attribute Types

Attributes passed to Web Components can only be one of the following:

  • String
  • Boolean
  • Number

Unlike in frameworks where you can pass arrays or objects, you can't do that with attributes in Web Components due to the limitations above. I know what you are thinking - why not just stringify/serialize the complex data type into a string? You do you! I wouldn't do that though. There are a few options: you can make them properties instead or think about your Web Components a bit differently to fit into "The Platform™" patterns. Would composition in your components help? Hopefully!

👀 Boolean Attributes

It's important to call out Boolean attributes in particular. In React, you may have something like the following for a checkbox.

const [isChecked, setIsChecked] = React.useState(false);

return <input type="checkbox" checked={isChecked} />;

Super convenient! Your state isChecked is a Boolean that's either true or false and it acts just as you'd expect.

Now open up a codepen and paste the following code.

<input type="checkbox" checked="false" />

The checked attribute is a Boolean type. The snippet above provided the checked attribute with a string value of false. I've asked a lot of people what their expectations are for this and the answer is normally that it shouldn't be checked.

It appears to be checked though. Why is that?

The presence of a boolean attribute applies that attribute. Simply setting checked="false" enables the checked attribute and the string value is ignored - even though it is set to "false". This is maybe unexpected given the React example above.

In the React example, we are using a Boolean value, whereas in the input example above we are using a string for the value. This is where the difference lies; however, it could be a bit confusing at first. React does some magic for you to automatically handle this case by removing the attribute when the value is falsy - how nice of them! But it could make things confusing when it comes to Boolean attributes when you're building without a framework.

So the tl;dr of this section is: if you don't want to activate a boolean attribute on a native element or Web Component, don't add it at all!

"How I Learned To Love The Bomb Event Listeners"

Okay, you caught me. Yes, I've been listening to the new Glass Animals album and happen to love this song.

Based on the point above about the attribute types, this means you can't pass functions into components.

In React, you would maybe have something like this:

const handleClick = (e) => console.log('clicked!', e);

return <ProductCard onClick={handleClick} />;

How does that look with Web Components? Events! You attach event listeners and when those events are dispatched, your callback is called.

document
  .querySelector('product-card')
  .addEventListener('click', (e) => console.log('clicked', e));

Internally, Web Components can dispatch their own events or custom events when something happens that a consumer should be notified of. This is identical thinking to passing a function into a component, but the difference is that the component itself dispatches the event and the consumer must add an event listener for it to be notified when "the thing" happens. In Lit, this is accomplished with this.dispatchEvent.

// Within my Web Component

function onSomeEventHappening() {
  // Plain old event
  this.dispatchEvent(new Event('cool-event'));

  // Or custom events, where you can provide
  // some extra details to the consumer!
  this.dispatchEvent(new CustomEvent('cool-custom-event'), {
    detail: 'some info',
  });
}

Attach event listeners to the custom events and you're all set.

document
  .querySelector('my-component')
  .addEventListener('cool-event', (e) => console.log(e));

document
  .querySelector('my-component')
  .addEventListener('cool-custom-event', (e) => console.log(e.target.detail));

For an example of what I'm talking about, take a look at this playground.

Styling

I already wrote about the advantages Design Systems have when it comes to styling with Web Components due to the Shadow DOM. Essentially Web Components need to make styling part of their public API either via CSS Custom Properties (variables) or CSS parts. Unlike in React, your components can't accept a class or className property that gets applied as only CSS variables pierce the Shadow DOM. Instead, stick to the blessed Platform way.

Slots

Slots are great! As a Web Component author, you get to decide areas where consumers can supply their own markup and it goes in those specific locations you identified.

The slot HTML element—part of the Web Components technology suite—is a placeholder inside a web component that you can fill with your own markup, which lets you create separate DOM trees and present them together.

Say you had a header component and you wanted to expose a way to render elements either on the left or right side. You do that with slots in web components. Here's how consuming a component with slots may look.

<my-header>
  <home-button slot="left"></home-button>

  <sign-out-button slot="right"></sign-out-button>
</my-header>

The home button goes on the left, the sign out button now sits on the right. The my-header component would look something like this.

<header style="display: flex; justify-content: space-between;">
  <!-- Left side container -->
  <div>
    <slot name="left"></slot>
  </div>

  <!-- Right side container -->
  <div>
    <slot name="right"></slot>
  </div>
</header>

We're relying on CSS a bit here to simplify putting things in their respective places, but you could imagine a more complex component where custom content needs to go in very specific places. By exposing slots, you get to define those locations! Pretty cool. I dig them.

No Self-Closing Tags

Web Components don't use self-closing tags, which is maybe a bit of a surprise if you're coming from a framework.

So in React you may have something like the following.

<MyCoolComponent prop1="Hello" prop2="World" />

With Web Components, this would need to look like this.

<my-cool-component attr1="Hello" attr2="World"></my-cool-component>

Another great read from Jake on some history here. There's also more info at MDN about void elements and self-closing tags. I could write more about this topic, but these two resources combined do such an excellent job that you should read these instead!

More...?

As time progresses, I'll add more here with amendments below. For now, that's all that comes to mind. Let me know if there's anything I missed or worth discussing! Enjoy! 👋

Reading Material

Here are some links that may be helpful.