A Web Components Primer
On the eve of February, I was inspired to tweet about web components. What started as a simple thought quickly turned into a series of tweets that folks seem to find useful. I've adapted the thread and I'm posting it here for prosperity.
Overview #
Shadow DOM? Light DOM? Slots? If you've heard of these but aren't sure what they are, maybe this will clear things up!
Custom elements are a feature that's baked into modern web browsers. You can build portable, reusable elements and use them in any HTML page. The collection of browser APIs that make this possible is known as web components.
Custom elements are written in JavaScript and must be registered at runtime. A custom element's tag name must start with a-z and contain at least one dash, e.g. <my-button>
.
Shadow DOM #
One of the best (and least understood) features of web components is the ability to encapsulate styles so nothing leaks in and nothing leaks out. This is done by attaching a hidden, separate DOM to the custom element.
This hidden DOM is called a "shadow DOM" or "shadow root."
The shadow root lets you use simple class names and ids without worrying if other elements on the page are using the same ones. (No more BEM!)
You can attach styles and scripts to a shadow root, too. It's kinda like a mini webpage within a webpage…minus the <iframe>
. 😂
The Host Element #
When you attach a shadow root to a custom element, the custom element acts as the host. Thus, we call it the "host element." Within a shadow root, you can target the host element in CSS using the :host()
selector. (OK, it's technically a function.)
The host element is just another HTML element, but you control its API. You control what attributes it can have, what properties it uses, and what it renders.
You can even control the content that gets "slotted in."
Slots #
Within a shadow root, you can create placeholders called slots. Slots let you control where child elements will be rendered in the template. The default slot looks like this in the custom element's template:
<slot></slot>
Additional slots can be added, but must be named. In the template, that looks like this.
<slot name="header"></slot>
Given the following custom element, the default slot will be populated with the paragraph and the header
slot will be populated with the <h2>
.
<my-element>
<h2 slot="header">Named Slot Example</h2>
<p>Lorem ipsum</p>
</my-element>
Everything you see here above is rendered in what we call the "light DOM."
Light DOM #
You can style things in the light DOM as you'd expect using simple CSS selectors. What you don't see is the internal HTML structure of the custom element. That stuff exists in the shadow DOM and isn't exposed for styling. This is what seems to confuse and frustrate people.
By default, you can't style anything other than a few inherited font properties. That doesn't sound useful at first, but the browser gives us ways to "poke through" the shadow root and apply styles.
That's right. As a custom element author, you can tell users what they can and can't change in the shadow root — and enforce it!
There are two primary tools we lean on for styling custom elements.
CSS Custom Properties #
Unlike regular CSS properties, CSS Custom Properties, or "CSS variables," cascade through shadow roots. They look kinda funny because they always start with two dashes.
:root {
--brand-color: blue;
}
Tip: the :root
selector above targets the <html>
element.
To reference a CSS variable in a stylesheet, we use the var()
function.
:host {
color: var(--brand-color);
}
CSS variables get interpolated, so the above will be interpreted by the browser as:
:host {
color: blue;
}
But unlike Sass, nothing gets compiled! Thus, if the variable changes at runtime, the browser happily updates everything using it. Did I mention that CSS variables cascade? You can redefine CSS variables in any selector, including pseudos such as :hover
and :focus
.
Of course, since we can't tap into the shadow DOM to style things, the custom element will need to "expose" which CSS variables it accepts as part of its API. This is a bummer, because the custom element author will need to expose a CSS variable for every property and state you might want to target.
If only we could style specific parts inside the shadow DOM. 🤔
CSS Parts #
Well, we can! A custom element can expose "parts" which are aptly called CSS Parts. Parts are also defined in the custom element's template, this time through the part
attribute.
<div part="container">
...
</div>
This exposes a part in the shadow root called container
that consumers can target with CSS.
my-element::part(container) {
background: blue:
color: white;
}
Now you can style any property you want on that part, including states such as :hover
and :focus
!
my-element::part(container):hover {
/* ... */
}
my-element::part(container):focus {
/* ... */
}
To recap: a CSS variable allows users to customize a single property and a CSS part allows them to customize all properties on the exposed part. When authoring custom elements, it's not always clear which one to use and when. The rule of thumb I follow is:
- When a single value gets reused throughout a component's styles, a CSS custom property is usually fine
- When you need to expose more than a handful of properties on a single element, a CSS part is usually better
"Why can't I just style things like normal?" #
That's a great question. I'm glad you asked…
Custom elements give us a way to build complex components with strong API contracts. Component authors can freely refactor internals without changing the public API. That wouldn't be possible if everything was exposed by default. Most things would be a breaking change. 😭
Style and logic encapsulation has been the holy grail of web development for a long time. Many solutions have been implemented to fake it, but elements have always been susceptible to leaks. The platform has finally given us a tool to solve this problem, so it's worth taking the time to learn and understand it. Once you do, I'm certain you'll embrace it!
"Web components sound kinda complicated!" #
Perhaps at first, but if you know HTML you're halfway there! Plus, these are standard browser features and, unlike framework knowledge, learning this stuff will last you a long, long time.
Think of all the frameworks you've learned over the years and no longer use because their popularity declined. The nice thing about web components is that browsers have committed to supporting them for a long time!
The spec will surely evolve, but the rug won't be pulled out from under you.
"How do you write web components?" #
You can write them with plain ol' JavaScript. Or you can use one of the many great component authoring libraries such as Google's Lit. React users might like Haunted for its hook-like syntax. Functional programmers might prefer Hybrids. There's also Microsoft's FAST Element and many others.
It's worth mentioning that both Svelte and Vue let you generate custom elements too!
Each library/framework has its own philosophy, but they all generate web components that work in any framework — and in plain ol' HTML pages.
The beautiful thing about this is you're not forced into a specific flavor. You can write web components the way you want to write them without sacrificing interoperability!
"Does anyone even use web components?" #
Yes. A lot of big companies are using them and they're becoming ubiquitous. Recently, over 18% of pages loaded in Chrome registered at least one web component.
Fun fact: Adobe is using web components for the web-based version of Photoshop and other apps that used to be desktop-first are transitioning.
"But I like my React/Vue/Angular!" #
That's cool! I like frameworks too, but I'm tired of rebuilding the same components every couple years. Most frameworks play nicely with web components, so you can have your cake and eat it too!
While most web components work perfectly fine as-is inside various frameworks, there is one notable exception.
React is super popular but it kinda doesn't get web components. What can we do?! No worries, React has added experimental support for custom elements behind a flag. A lot of progress has been made here recently!
In the meantime, you can wrap any custom element for React with a simple function call. This will generate a real React component that wires things up to the underlying custom element. It's like magic! (Tip: the utility has Lit in the name, but it works with all custom elements.)
How to Get Started #
Hopefully you learned something about web components and shadow DOM in this thread! If you haven't experimented with them yet, why not? It's worth your time to better understand them. Even if you're not developing them yet, you'll likely be using them soon.
One great way to jump in is by using a library. I happen to be the author of Shoelace, an open source web component library featuring more than 50 useful components.
This is a great way to get started with custom elements and, because it's open source, you can dive into the code and start learning how they're made too!
Have questions? Want to learn more about web components? Follow me on Twitter for more tips, resources, and other web dev stuff!